Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e4b91f89b |
No files matched your search
@@ -1,5 +1,4 @@
|
||||
node_modules/
|
||||
node_modules.prev/
|
||||
.turbo/
|
||||
.next/
|
||||
.next.prev/
|
||||
|
||||
@@ -7,7 +7,16 @@
|
||||
"src/app/**/{error,not-found,loading,template,default,global-error}.{ts,tsx}",
|
||||
"scripts/migrate-aes-cbc-to-gcm.ts"
|
||||
],
|
||||
"project": ["src/**/*.{ts,tsx,css}", "scripts/**/*.{ts,js}"],
|
||||
"ignoreDependencies": ["@sentry/nextjs", "pino-pretty", "husky"],
|
||||
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.{ts,js}"],
|
||||
"ignoreDependencies": [
|
||||
"tailwindcss-animate",
|
||||
"@tailwindcss/forms",
|
||||
"@tailwindcss/typography",
|
||||
"pino-pretty",
|
||||
"tailwindcss",
|
||||
"husky",
|
||||
"@sentry/nextjs"
|
||||
],
|
||||
"ignore": ["src/actions/import-badges.ts"],
|
||||
"ignoreBinaries": ["sendmail"]
|
||||
}
|
||||
+1
-1
@@ -36,7 +36,7 @@ const securityHeaders = [
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true, // <-- Dit activeert Cache Components en lost de 'instant' build-fout op!
|
||||
deploymentId: resolveDeploymentId(),
|
||||
serverExternalPackages: ["lzma", "sharp", "pino", "pino-pretty"],
|
||||
serverExternalPackages: ["mariadb", "lzma", "sharp", "pino", "pino-pretty"],
|
||||
|
||||
// Onderdruk de specifieke Turbopack waarschuwingen voor brede bestandspatronen in de lib-map
|
||||
turbopack: {
|
||||
|
||||
+2
-1
@@ -31,7 +31,6 @@
|
||||
"@dnd-kit/sortable": "10.0.0",
|
||||
"@dnd-kit/utilities": "3.2.2",
|
||||
"@hookform/resolvers": "5.7.1",
|
||||
"@next/bundle-analyzer": "16.3.0",
|
||||
"@tanstack/react-virtual": "3.14.9",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
@@ -68,6 +67,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.7",
|
||||
"@next/bundle-analyzer": "16.3.0",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
@@ -75,6 +75,7 @@
|
||||
"@types/react": "19.2.18",
|
||||
"@types/react-dom": "19.2.4",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"husky": "^9.1.7",
|
||||
|
||||
Generated
+6
-4
@@ -37,9 +37,6 @@ importers:
|
||||
'@hookform/resolvers':
|
||||
specifier: 5.7.1
|
||||
version: 5.7.1(@standard-schema/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@next/bundle-analyzer':
|
||||
specifier: 16.3.0
|
||||
version: 16.3.0
|
||||
'@tanstack/react-virtual':
|
||||
specifier: 3.14.9
|
||||
version: 3.14.9([email protected]([email protected]))([email protected])
|
||||
@@ -143,6 +140,9 @@ importers:
|
||||
'@biomejs/biome':
|
||||
specifier: 2.5.7
|
||||
version: 2.5.7
|
||||
'@next/bundle-analyzer':
|
||||
specifier: 16.3.0
|
||||
version: 16.3.0
|
||||
'@tailwindcss/forms':
|
||||
specifier: ^0.5.11
|
||||
version: 0.5.11([email protected])
|
||||
@@ -164,6 +164,9 @@ importers:
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 4.1.10
|
||||
version: 4.1.10([email protected])
|
||||
babel-plugin-react-compiler:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
dotenv:
|
||||
specifier: ^17.4.2
|
||||
version: 17.4.2
|
||||
@@ -4316,7 +4319,6 @@ snapshots:
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@babel/types': 7.29.8
|
||||
optional: true
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"use server";
|
||||
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { importBadgeSynced } from "@/lib/services/import-badge";
|
||||
|
||||
export async function importBadgeFromRemote({
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
}: {
|
||||
code: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}) {
|
||||
await requirePermission(PERMS.ASSETS_IMPORT);
|
||||
const result = await importBadgeSynced({ code, name, description });
|
||||
if (!result.ok) {
|
||||
return { ok: false as const, error: result.error };
|
||||
}
|
||||
return { ok: true as const };
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { Languages, Package, Settings } from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { type CatalogItemData, CatalogItemsTable } from "./catalog-items-table";
|
||||
import type { CatalogItemData } from "./catalog-items-table";
|
||||
import { CatalogItemsTable } from "./catalog-items-table";
|
||||
import { CatalogPageForm } from "./catalog-page-form";
|
||||
import { CatalogTranslateTab } from "./catalog-translate-tab";
|
||||
|
||||
|
||||
@@ -90,14 +90,8 @@ import {
|
||||
} from "./field-helpers";
|
||||
import { FurniIcon } from "./furni-icon";
|
||||
import { SongPicker } from "./song-picker";
|
||||
import {
|
||||
type BaseItemData,
|
||||
type CatalogItemData,
|
||||
DEFAULT_NEW_ITEM,
|
||||
getPointsLabel,
|
||||
type InlineEdit,
|
||||
POINTS_TYPES,
|
||||
} from "./types";
|
||||
import type { BaseItemData, CatalogItemData, InlineEdit } from "./types";
|
||||
import { DEFAULT_NEW_ITEM, getPointsLabel, POINTS_TYPES } from "./types";
|
||||
|
||||
export type { BaseItemData, CatalogItemData };
|
||||
|
||||
|
||||
@@ -11,14 +11,13 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
getCachedSongPickerOptions,
|
||||
invalidateSongPickerCache,
|
||||
loadSongPickerOptions,
|
||||
type SoundtrackOption,
|
||||
} from "@/lib/client-cache/song-picker-cache";
|
||||
|
||||
// Re-export so existing `import { invalidateSongPickerCache } from '.../song-picker'`
|
||||
// callsites keep working. New code should import from the lib module directly.
|
||||
export { invalidateSongPickerCache };
|
||||
export { invalidateSongPickerCache } from "@/lib/client-cache/song-picker-cache";
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
|
||||
@@ -39,56 +39,30 @@ interface NitroEditorDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface NitroVisualization {
|
||||
size?: number;
|
||||
layers?: Record<string, { z?: number; alpha?: number; ink?: string }>;
|
||||
colors?: Record<string, Record<string, { color: number }>>;
|
||||
animations?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// The .nitro metadata is untyped JSON from disk; typed only for the fields the
|
||||
// editor reads/writes. Unknown keys are traversed via the path helpers below.
|
||||
interface NitroMetadata {
|
||||
visualizationType?: string;
|
||||
logicType?: string;
|
||||
logic?: {
|
||||
model?: {
|
||||
dimensions?: { x: number; y: number; z: number };
|
||||
directions?: number[];
|
||||
};
|
||||
};
|
||||
visualizations?: NitroVisualization[];
|
||||
}
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
function updateNested<T extends object>(
|
||||
obj: T,
|
||||
path: string,
|
||||
value: unknown,
|
||||
): T {
|
||||
const clone: T = JSON.parse(JSON.stringify(obj));
|
||||
// Keep these helpers on `any` — Next's build-time checker has repeatedly
|
||||
// disagreed with `tsc` on stricter JSON typings for this editor.
|
||||
// biome-ignore lint/suspicious/noExplicitAny: metadata tree is untyped JSON
|
||||
function updateNested(obj: any, path: string, value: unknown): any {
|
||||
const clone = JSON.parse(JSON.stringify(obj));
|
||||
const keys = path.split(".");
|
||||
let current: JsonObject = clone as JsonObject;
|
||||
let current = clone;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const next = current[keys[i]];
|
||||
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
||||
current[keys[i]] = {};
|
||||
}
|
||||
current = current[keys[i]] as JsonObject;
|
||||
if (!current[keys[i]]) current[keys[i]] = {};
|
||||
current = current[keys[i]];
|
||||
}
|
||||
current[keys[keys.length - 1]] = value;
|
||||
return clone;
|
||||
}
|
||||
|
||||
function getNested<T>(obj: unknown, path: string, fallback: T): T {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: metadata tree is untyped JSON
|
||||
function getNested(obj: any, path: string, fallback?: unknown): any {
|
||||
const keys = path.split(".");
|
||||
let current: unknown = obj;
|
||||
let current = obj;
|
||||
for (const key of keys) {
|
||||
if (current == null || typeof current !== "object") return fallback;
|
||||
current = (current as JsonObject)[key];
|
||||
current = current[key];
|
||||
}
|
||||
return current === undefined ? fallback : (current as T);
|
||||
return current ?? fallback;
|
||||
}
|
||||
|
||||
const ALL_DIRECTIONS = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
@@ -125,9 +99,10 @@ export function NitroEditorDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: NitroEditorDialogProps) {
|
||||
const [metadata, setMetadata] = useState<NitroMetadata | null>(null);
|
||||
const [originalMetadata, setOriginalMetadata] =
|
||||
useState<NitroMetadata | null>(null);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code
|
||||
const [metadata, setMetadata] = useState<any>(null);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code
|
||||
const [originalMetadata, setOriginalMetadata] = useState<any>(null);
|
||||
const [jsonText, setJsonText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -145,9 +120,10 @@ export function NitroEditorDialog({
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
|
||||
const patchMeta = (path: string, value: unknown) => {
|
||||
setMetadata((prev) =>
|
||||
prev == null ? prev : updateNested(prev, path, value),
|
||||
);
|
||||
// Explicit any avoids noImplicitAny on updater params and dodges
|
||||
// Next build's stricter JSON inference that disagreed with tsc.
|
||||
// biome-ignore lint/suspicious/noExplicitAny: metadata tree is untyped JSON
|
||||
setMetadata((prev: any) => updateNested(prev, path, value));
|
||||
};
|
||||
|
||||
const fetchMetadata = useCallback(async () => {
|
||||
@@ -253,12 +229,14 @@ export function NitroEditorDialog({
|
||||
|
||||
// ── Visual tab helpers ──
|
||||
|
||||
const dims = metadata
|
||||
? getNested(metadata, "logic.model.dimensions", { x: 1, y: 1, z: 0 })
|
||||
: { x: 1, y: 1, z: 0 };
|
||||
const directions = metadata
|
||||
? getNested(metadata, "logic.model.directions", [0])
|
||||
: [0];
|
||||
const dims = (
|
||||
metadata
|
||||
? getNested(metadata, "logic.model.dimensions", { x: 1, y: 1, z: 0 })
|
||||
: { x: 1, y: 1, z: 0 }
|
||||
) as { x: number; y: number; z: number };
|
||||
const directions = (
|
||||
metadata ? getNested(metadata, "logic.model.directions", [0]) : [0]
|
||||
) as number[];
|
||||
|
||||
const setDim = (axis: "x" | "y" | "z", value: string) => {
|
||||
const num =
|
||||
@@ -267,7 +245,11 @@ export function NitroEditorDialog({
|
||||
};
|
||||
|
||||
const toggleDirection = (dir: number) => {
|
||||
const current = getNested(metadata, "logic.model.directions", [0]);
|
||||
const current = getNested(
|
||||
metadata,
|
||||
"logic.model.directions",
|
||||
[0],
|
||||
) as number[];
|
||||
const next = current.includes(dir)
|
||||
? current.filter((d: number) => d !== dir)
|
||||
: [...current, dir].sort((a: number, b: number) => a - b);
|
||||
@@ -276,9 +258,12 @@ export function NitroEditorDialog({
|
||||
};
|
||||
|
||||
// Get visualization with size=64 for layer/color editing
|
||||
const viz64 = metadata?.visualizations?.find((v) => v.size === 64);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code
|
||||
const viz64 = metadata?.visualizations?.find((v: any) => v.size === 64);
|
||||
const viz64Index =
|
||||
metadata?.visualizations?.findIndex((v) => v.size === 64) ?? -1;
|
||||
metadata?.visualizations?.findIndex(
|
||||
(v: { size: number }) => v.size === 64,
|
||||
) ?? -1;
|
||||
|
||||
const layerCount = viz64?.layers ? Object.keys(viz64.layers).length : 0;
|
||||
const colorCount = viz64?.colors ? Object.keys(viz64.colors).length : 0;
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
@plugin "tailwindcss-animate";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
:root {
|
||||
--font-nunito:
|
||||
"Nunito", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--font-pixel: "Pixelify Sans", monospace;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans:
|
||||
var(--font-nunito), "Nunito", ui-sans-serif, system-ui, -apple-system,
|
||||
|
||||
+15
-20
@@ -1,5 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Nunito, Pixelify_Sans } from "next/font/google";
|
||||
import { headers } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
@@ -15,20 +14,6 @@ import { resolveHotelName } from "@/lib/hotel-name";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import "./globals.css";
|
||||
|
||||
const nunito = Nunito({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "600", "700", "800"],
|
||||
variable: "--font-nunito",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const pixelify = Pixelify_Sans({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
variable: "--font-pixel",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
// The root layout reads runtime data (locale cookie, request headers via
|
||||
// enforceSiteAccess, DB-backed site settings) to render the <html> element,
|
||||
// which cannot be wrapped in <Suspense>. Child pages inherit this opt-out,
|
||||
@@ -73,13 +58,23 @@ export default async function RootLayout({
|
||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
className={`app ${nunito.variable} ${pixelify.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<html lang={locale} className="app" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="theme-default-dark" content={String(defaultDark)} />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@400;700&display=swap"
|
||||
/>
|
||||
<Script
|
||||
src="/scripts/theme-init.js"
|
||||
strategy="beforeInteractive"
|
||||
|
||||
@@ -16,10 +16,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { updateCatalogPage } from "@/actions/catalog";
|
||||
import { updateBcPage } from "@/actions/catalog-bc";
|
||||
import {
|
||||
type CatalogItemData,
|
||||
CatalogItemsTable,
|
||||
} from "@/app/admin/catalog/[id]/catalog-items-table";
|
||||
import type { CatalogItemData } from "@/app/admin/catalog/[id]/catalog-items-table";
|
||||
import { CatalogItemsTable } from "@/app/admin/catalog/[id]/catalog-items-table";
|
||||
import { CatalogTranslateTab } from "@/app/admin/catalog/[id]/catalog-translate-tab";
|
||||
import { BulkImportItems } from "@/components/admin/catalog/bulk-import-items";
|
||||
import { CatalogImagePicker } from "@/components/admin/catalog/catalog-image-picker";
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Crown, Image as ImageIcon, Package, Sparkles } from "lucide-react";
|
||||
import {
|
||||
type CatalogItemData,
|
||||
getPointsLabel,
|
||||
} from "@/app/admin/catalog/[id]/catalog-items-table/types";
|
||||
import type { CatalogItemData } from "@/app/admin/catalog/[id]/catalog-items-table/types";
|
||||
import { getPointsLabel } from "@/app/admin/catalog/[id]/catalog-items-table/types";
|
||||
import {
|
||||
CurrencyIcon,
|
||||
currencyKindFromPointsType,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
Languages,
|
||||
LayoutDashboard,
|
||||
ListOrdered,
|
||||
type LucideIcon,
|
||||
Megaphone,
|
||||
Monitor,
|
||||
Newspaper,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { logAuthorizationEvent } from "@/lib/admin/authorization-events";
|
||||
import { validateCsrfToken } from "@/lib/foundation/security";
|
||||
import { canAccess, getApiAdminContext } from "@/lib/permissions";
|
||||
|
||||
@@ -45,6 +45,9 @@ describe("admin import backend contract", () => {
|
||||
expect(service).toContain("ExternalTexts.json");
|
||||
expect(service).toContain("WebsiteBadges");
|
||||
expect(service).toContain("importBadgeSynced");
|
||||
expect(readFileSync("src/actions/import-badges.ts", "utf8")).toContain(
|
||||
"importBadgeSynced",
|
||||
);
|
||||
expect(
|
||||
readFileSync("src/app/api/admin/import/badges/route.ts", "utf8"),
|
||||
).toContain("importBadgeSynced");
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { EffectMapEntry } from "@/types/effects";
|
||||
import {
|
||||
__setEffectMapPathForTests,
|
||||
listLocalEffects,
|
||||
mergeEffect,
|
||||
readEffectMap,
|
||||
removeEffect,
|
||||
} from "./effectmap";
|
||||
|
||||
vi.mock("@/lib/services/site-settings", () => ({
|
||||
siteSettings: { get: vi.fn(async () => "") },
|
||||
}));
|
||||
|
||||
let dir: string;
|
||||
let file: string;
|
||||
|
||||
const ENTRY: EffectMapEntry = {
|
||||
id: "fx_1",
|
||||
lib: "fx_1",
|
||||
type: "fx",
|
||||
revision: 1,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), "effectmap-"));
|
||||
file = path.join(dir, "EffectMap.json");
|
||||
await fs.writeFile(file, JSON.stringify({ effects: [ENTRY] }));
|
||||
__setEffectMapPathForTests(file);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
__setEffectMapPathForTests(null);
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("lib/services/effectmap", () => {
|
||||
it("lists local effects from the map", async () => {
|
||||
const effects = await listLocalEffects();
|
||||
expect(effects).toEqual([ENTRY]);
|
||||
});
|
||||
|
||||
it("returns an empty list when the file has no effects array", async () => {
|
||||
await fs.writeFile(file, JSON.stringify({}));
|
||||
expect(await readEffectMap()).toEqual({ effects: [] });
|
||||
});
|
||||
|
||||
it("adds a new effect via mergeEffect", async () => {
|
||||
const added: EffectMapEntry = {
|
||||
id: "fx_2",
|
||||
lib: "fx_2",
|
||||
type: "fx",
|
||||
revision: 2,
|
||||
};
|
||||
await mergeEffect(added);
|
||||
const effects = await listLocalEffects();
|
||||
expect(effects).toHaveLength(2);
|
||||
expect(effects).toContainEqual(added);
|
||||
});
|
||||
|
||||
it("updates an existing effect in place via mergeEffect", async () => {
|
||||
await mergeEffect({ ...ENTRY, revision: 5 });
|
||||
const effects = await listLocalEffects();
|
||||
expect(effects).toHaveLength(1);
|
||||
expect(effects[0].revision).toBe(5);
|
||||
});
|
||||
|
||||
it("removes an effect by id", async () => {
|
||||
expect(await removeEffect("fx_1")).toBe(true);
|
||||
expect(await listLocalEffects()).toEqual([]);
|
||||
});
|
||||
|
||||
it("removes an effect by lib name", async () => {
|
||||
expect(await removeEffect("fx_1")).toBe(true);
|
||||
expect(await listLocalEffects()).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports false when nothing was removed", async () => {
|
||||
expect(await removeEffect("missing")).toBe(false);
|
||||
expect(await listLocalEffects()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FigureLibrary } from "./figuremap";
|
||||
import {
|
||||
__setFigureMapPathForTests,
|
||||
listFigureLibraries,
|
||||
mergeLibrary,
|
||||
} from "./figuremap";
|
||||
|
||||
vi.mock("@/lib/services/site-settings", () => ({
|
||||
siteSettings: { get: vi.fn(async () => "") },
|
||||
}));
|
||||
|
||||
let dir: string;
|
||||
let file: string;
|
||||
|
||||
const LIB: FigureLibrary = {
|
||||
id: "h_human",
|
||||
revision: 1,
|
||||
parts: [{ id: 1, type: "hd" }],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), "figuremap-"));
|
||||
file = path.join(dir, "FigureMap.json");
|
||||
await fs.writeFile(file, JSON.stringify({ libraries: [LIB] }));
|
||||
__setFigureMapPathForTests(file);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
__setFigureMapPathForTests(null);
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("lib/services/figuremap", () => {
|
||||
it("lists figure libraries from the map", async () => {
|
||||
const libraries = await listFigureLibraries();
|
||||
expect(libraries).toEqual([LIB]);
|
||||
});
|
||||
|
||||
it("returns an empty list when the file has no libraries array", async () => {
|
||||
await fs.writeFile(file, JSON.stringify({}));
|
||||
expect(await listFigureLibraries()).toEqual([]);
|
||||
});
|
||||
|
||||
it("adds a new library via mergeLibrary", async () => {
|
||||
const added: FigureLibrary = { id: "hh_human", revision: 2 };
|
||||
await mergeLibrary(added);
|
||||
const libraries = await listFigureLibraries();
|
||||
expect(libraries).toHaveLength(2);
|
||||
expect(libraries).toContainEqual(added);
|
||||
});
|
||||
|
||||
it("updates an existing library in place via mergeLibrary", async () => {
|
||||
await mergeLibrary({ ...LIB, revision: 9 });
|
||||
const libraries = await listFigureLibraries();
|
||||
expect(libraries).toHaveLength(1);
|
||||
expect(libraries[0].revision).toBe(9);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,23 @@ import { and, eq, type SQL, sql } from "drizzle-orm";
|
||||
import { CatalogPages, db, ItemsBase } from "@/lib/db";
|
||||
import { officialHabboEnrichmentWarning } from "@/lib/habbo-gamedata-hotel";
|
||||
import { logger } from "@/lib/logger";
|
||||
import {
|
||||
appendFurniEntry,
|
||||
buildFurniEntry,
|
||||
removeFurniEntry,
|
||||
} from "@/lib/services/furni-data";
|
||||
import type { OfficialHabboFurniEntry } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { lookupOfficialHabboFurni } from "@/lib/services/habbo-furnidata-cache";
|
||||
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
||||
|
||||
// Re-export the download helpers now owned by the shared import core.
|
||||
// Existing furni callers keep importing them from here.
|
||||
export {
|
||||
downloadFile,
|
||||
validatePngBytes,
|
||||
validateSwfBytes,
|
||||
} from "@/lib/services/import/core/download";
|
||||
|
||||
import {
|
||||
DEFAULT_FURNI_ICON_DIR,
|
||||
DEFAULT_FURNI_NITRO_DIR,
|
||||
@@ -11,32 +28,14 @@ import {
|
||||
getFurniAssetDirs,
|
||||
getFurniAssetWriteTargets,
|
||||
} from "@/lib/services/furni-asset-dirs";
|
||||
import { downloadFile } from "@/lib/services/import/core/download";
|
||||
import type { ConversionResult } from "@/lib/services/swf-to-nitro";
|
||||
import {
|
||||
appendFurniEntry,
|
||||
buildFurniEntry,
|
||||
removeFurniEntry,
|
||||
} from "@/lib/services/furni-data";
|
||||
import {
|
||||
lookupOfficialHabboFurni,
|
||||
type OfficialHabboFurniEntry,
|
||||
} from "@/lib/services/habbo-furnidata-cache";
|
||||
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
||||
import {
|
||||
downloadFile,
|
||||
validatePngBytes,
|
||||
validateSwfBytes,
|
||||
} from "@/lib/services/import/core/download";
|
||||
import {
|
||||
type ConversionResult,
|
||||
convertSwfToNitro,
|
||||
extractIconFromSwf,
|
||||
} from "@/lib/services/swf-to-nitro";
|
||||
import type { ImportSingleResult } from "@/types/furni";
|
||||
|
||||
// Re-export the download helpers now owned by the shared import core.
|
||||
// Existing furni callers keep importing them from here.
|
||||
export { downloadFile, validatePngBytes, validateSwfBytes };
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────
|
||||
|
||||
// turbopackIgnore hints: this module is reached by App Router routes; without
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { type CurrencyDb, sendCurrency } from "./send-currency";
|
||||
import type { CurrencyDb } from "./send-currency";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {},
|
||||
@@ -9,6 +9,8 @@ vi.mock("@/lib/db", () => ({
|
||||
|
||||
vi.mock("@/lib/services/rcon", () => ({}));
|
||||
|
||||
import { sendCurrency } from "./send-currency";
|
||||
|
||||
const mockDb = {} as CurrencyDb;
|
||||
|
||||
describe("sendCurrency", () => {
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import { deflateSync } from "node:zlib";
|
||||
import LZMA from "lzma";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
decompressSwf,
|
||||
parseBinaryData,
|
||||
parseSwfTags,
|
||||
parseSymbolClass,
|
||||
TAG_DEFINE_BINARY_DATA,
|
||||
TAG_DEFINE_BITS_LOSSLESS,
|
||||
TAG_END,
|
||||
} from "./swf-parser";
|
||||
|
||||
const BODY = Buffer.from(
|
||||
"uncompressed swf body used for decompression fixtures, padded to a reasonable length",
|
||||
);
|
||||
|
||||
function fwsHeader(payload: Buffer, version = 6): Buffer {
|
||||
const header = Buffer.alloc(8);
|
||||
header.write("FWS", 0, "ascii");
|
||||
header.writeUInt8(version, 3);
|
||||
header.writeUInt32LE(payload.length + 8, 4);
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
function cwsFixture(payload: Buffer, version = 6): Buffer {
|
||||
const header = Buffer.alloc(8);
|
||||
header.write("CWS", 0, "ascii");
|
||||
header.writeUInt8(version, 3);
|
||||
header.writeUInt32LE(payload.length + 8, 4);
|
||||
const compressed = deflateSync(payload);
|
||||
return Buffer.concat([header, compressed]);
|
||||
}
|
||||
|
||||
function zwsFixture(payload: Buffer, version = 6): Buffer {
|
||||
const rawLzma = LZMA.compress(payload, 9);
|
||||
const props = Buffer.from(rawLzma.slice(0, 5));
|
||||
const compressedPayload = Buffer.from(rawLzma.slice(13));
|
||||
const compressedSize = 5 + compressedPayload.length;
|
||||
const header = Buffer.alloc(12);
|
||||
header.write("ZWS", 0, "ascii");
|
||||
header.writeUInt8(version, 3);
|
||||
header.writeUInt32LE(payload.length + 8, 4);
|
||||
header.writeUInt32LE(compressedSize, 8);
|
||||
return Buffer.concat([header, props, compressedPayload]);
|
||||
}
|
||||
|
||||
describe("decompressSwf", () => {
|
||||
it("returns FWS buffers untouched", () => {
|
||||
const fws = fwsHeader(BODY);
|
||||
expect(decompressSwf(fws)).toBe(fws);
|
||||
});
|
||||
|
||||
it("inflates CWS (zlib) buffers and restores the FWS signature", () => {
|
||||
const result = decompressSwf(cwsFixture(BODY));
|
||||
expect(result.toString("ascii", 0, 3)).toBe("FWS");
|
||||
expect(result.length).toBe(BODY.length + 8);
|
||||
expect(result.subarray(8).equals(BODY)).toBe(true);
|
||||
});
|
||||
|
||||
it("decompresses ZWS (lzma) buffers and restores the FWS signature", () => {
|
||||
const result = decompressSwf(zwsFixture(BODY));
|
||||
expect(result.toString("ascii", 0, 3)).toBe("FWS");
|
||||
expect(result.length).toBe(BODY.length + 8);
|
||||
expect(result.subarray(8).equals(BODY)).toBe(true);
|
||||
});
|
||||
|
||||
it("throws on unknown signatures", () => {
|
||||
const bad = Buffer.alloc(16);
|
||||
bad.write("XYZ", 0, "ascii");
|
||||
expect(() => decompressSwf(bad)).toThrow("Unknown SWF signature");
|
||||
});
|
||||
});
|
||||
|
||||
function shortTag(tagType: number, data: Buffer): Buffer {
|
||||
expect(data.length).toBeLessThan(0x3f);
|
||||
const tag = Buffer.alloc(2 + data.length);
|
||||
tag.writeUInt16LE((tagType << 6) | data.length, 0);
|
||||
data.copy(tag, 2);
|
||||
return tag;
|
||||
}
|
||||
|
||||
function swfWithTags(tags: Buffer[]): Buffer {
|
||||
const tagStream = Buffer.concat([...tags, Buffer.from([0, 0])]);
|
||||
const body = Buffer.alloc(13);
|
||||
body.write("FWS", 0, "ascii");
|
||||
body.writeUInt8(6, 3);
|
||||
body.writeUInt32LE(tagStream.length + 13, 4);
|
||||
return Buffer.concat([body, tagStream]);
|
||||
}
|
||||
|
||||
describe("parseSwfTags", () => {
|
||||
it("parses short-form tags until the end marker", () => {
|
||||
const swf = swfWithTags([
|
||||
shortTag(TAG_DEFINE_BITS_LOSSLESS, Buffer.from([1, 2, 3])),
|
||||
shortTag(TAG_DEFINE_BINARY_DATA, Buffer.from([9, 9, 9])),
|
||||
]);
|
||||
const tags = parseSwfTags(swf);
|
||||
expect(tags).toHaveLength(2);
|
||||
expect(tags[0].type).toBe(TAG_DEFINE_BITS_LOSSLESS);
|
||||
expect(tags[0].data.equals(Buffer.from([1, 2, 3]))).toBe(true);
|
||||
expect(tags[1].type).toBe(TAG_DEFINE_BINARY_DATA);
|
||||
});
|
||||
|
||||
it("stops at TAG_END and ignores trailing bytes", () => {
|
||||
const swf = swfWithTags([
|
||||
shortTag(TAG_DEFINE_BITS_LOSSLESS, Buffer.from([1])),
|
||||
]);
|
||||
expect(parseSwfTags(swf)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("returns an empty array for an empty tag stream", () => {
|
||||
const empty = Buffer.alloc(13);
|
||||
empty.write("FWS", 0, "ascii");
|
||||
expect(parseSwfTags(empty)).toEqual([]);
|
||||
expect(TAG_END).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSymbolClass", () => {
|
||||
it("maps char ids to names and tracks aliases", () => {
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([2, 0]),
|
||||
Buffer.from([1, 0]),
|
||||
Buffer.from("chair\0", "utf8"),
|
||||
Buffer.from([2, 0]),
|
||||
Buffer.from("table\0", "utf8"),
|
||||
]);
|
||||
const result = parseSymbolClass(data);
|
||||
expect(result.classNames.get(1)).toBe("chair");
|
||||
expect(result.classNames.get(2)).toBe("table");
|
||||
expect(result.symbols.get("chair")).toBe(1);
|
||||
expect(result.symbols.get("table")).toBe(2);
|
||||
});
|
||||
|
||||
it("keeps the first name for a repeated char id", () => {
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([2, 0]),
|
||||
Buffer.from([1, 0]),
|
||||
Buffer.from("chair\0", "utf8"),
|
||||
Buffer.from([1, 0]),
|
||||
Buffer.from("lounge_chair\0", "utf8"),
|
||||
]);
|
||||
const result = parseSymbolClass(data);
|
||||
expect(result.classNames.get(1)).toBe("chair");
|
||||
expect(result.symbols.get("lounge_chair")).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseBinaryData", () => {
|
||||
it("reads the char id and strips the 6-byte attribute header", () => {
|
||||
const data = Buffer.concat([
|
||||
Buffer.from([42, 0]),
|
||||
Buffer.from([1, 2, 3, 4]),
|
||||
Buffer.from("binary payload"),
|
||||
]);
|
||||
const result = parseBinaryData(data);
|
||||
expect(result.id).toBe(42);
|
||||
expect(result.content.equals(Buffer.from("binary payload"))).toBe(true);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -32,5 +32,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "e2e", "playwright.config.ts", "src/generated"]
|
||||
"exclude": ["node_modules", "e2e", "playwright.config.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user