diff --git a/next.config.ts b/next.config.ts index f7e5868d..7541a289 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,166 +1,7 @@ -import { execFileSync } from "node:child_process"; -import withBundleAnalyzer from "@next/bundle-analyzer"; import type { NextConfig } from "next"; -import createNextIntlPlugin from "next-intl/plugin"; - -function resolveDeploymentId(): string | undefined { - const configuredId = process.env.NEXT_DEPLOYMENT_ID?.trim(); - if (configuredId) return configuredId; - - try { - return execFileSync("git", ["rev-parse", "HEAD"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } catch { - return process.env.APP_VERSION?.trim() || undefined; - } -} - -const securityHeaders = [ - { key: "X-DNS-Prefetch-Control", value: "on" }, - { - key: "Strict-Transport-Security", - value: "max-age=63072000; includeSubDomains; preload", - }, - { key: "X-Frame-Options", value: "DENY" }, - { key: "X-Content-Type-Options", value: "nosniff" }, - { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, - { - key: "Permissions-Policy", - value: "camera=(), microphone=(), geolocation=(), interest-cohort=()", - }, - // CSP is set per-request in src/proxy.ts with a script nonce (no 'unsafe-inline' for scripts). -]; const nextConfig: NextConfig = { - cacheComponents: true, - deploymentId: resolveDeploymentId(), - serverExternalPackages: ["lzma", "sharp", "pino", "pino-pretty"], - - // The /admin/import hub was consolidated into the catalogue studio at - // /admin/studio. Redirect the old routes so bookmarks/links don't 404. - async redirects() { - return [ - { - source: "/admin/import", - destination: "/admin/studio/furni", - permanent: true, - }, - { - source: "/admin/import/badges", - destination: "/admin/studio/badges", - permanent: true, - }, - { - source: "/admin/import/furni", - destination: "/admin/studio/furni", - permanent: true, - }, - { - source: "/admin/import/furni/upload", - destination: "/admin/studio/upload", - permanent: true, - }, - { - source: "/admin/import/clothing", - destination: "/admin/studio/clothing", - permanent: true, - }, - { - source: "/admin/import/effects", - destination: "/admin/studio/effects", - permanent: true, - }, - { - source: "/admin/import/pets", - destination: "/admin/studio/pets", - permanent: true, - }, - { - source: "/admin/import/clone", - destination: "/admin/studio/clone", - permanent: true, - }, - { - source: "/admin/import/sync", - destination: "/admin/studio/sync", - permanent: true, - }, - { - source: "/admin/import/repair-icons", - destination: "/admin/studio/repair-icons", - permanent: true, - }, - { - source: "/admin/import/audit", - destination: "/admin/studio/audit", - permanent: true, - }, - ]; - }, - - // Silence Turbopack warnings for the broad file patterns in src/lib. - turbopack: { - ignoreIssue: [ - { - path: "**/src/lib/**", - }, - ], - }, - - typescript: { - ignoreBuildErrors: false, - }, - - // Compress responses with gzip/brotli. - compress: true, - - // Disable Next.js telemetry and browser sourcemaps in production. - productionBrowserSourceMaps: false, - - experimental: { - useTypeScriptCli: true, - hideLogsAfterAbort: true, - }, - - // Optimize images served through next/image with sharp → AVIF/WebP. - images: { - formats: ["image/avif", "image/webp"], - }, - - // Add caching headers for static assets - async headers() { - return [ - { - source: "/(.*)", - headers: securityHeaders, - }, - { - source: "/assets/(.*)", - headers: [ - { - key: "Cache-Control", - value: "public, max-age=31536000, immutable", - }, - ], - }, - { - source: "/images/(.*)", - headers: [{ key: "Cache-Control", value: "public, max-age=86400" }], - }, - ]; - }, + serverExternalPackages: ["lzma-js"], }; -// next-intl WITHOUT i18n routing — locale comes from the NEXT_LOCALE cookie via -// src/i18n/request.ts, so URLs and the access-guard middleware stay unchanged. -const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); - -const config = withNextIntl(nextConfig); - -const withBA = withBundleAnalyzer({ - enabled: process.env.ANALYZE === "true", -}); - -export default withBA(config); +export default nextConfig; diff --git a/package.json b/package.json index bdc4c8fa..b101870e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "jszip": "3.10.1", "lenis": "1.3.26", "lucide-react": "1.33.0", - "lzma": "2.3.2", + "lzma": "^2.3.2", + "lzma-js": "^1.0.1", "motion": "13.1.1", "music-metadata": "11.15.0", "mysql2": "3.23.4", @@ -75,6 +76,7 @@ "@types/react": "19.2.18", "@types/react-dom": "19.2.4", "@vitest/coverage-v8": "4.1.11", + "beasties": "^0.4.3", "drizzle-kit": "0.31.10", "husky": "9.1.7", "knip": "6.32.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fcebb17..a71ff7fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,8 +83,11 @@ importers: specifier: 1.33.0 version: 1.33.0(react@19.2.8) lzma: - specifier: 2.3.2 + specifier: ^2.3.2 version: 2.3.2 + lzma-js: + specifier: ^1.0.1 + version: 1.0.1 motion: specifier: 13.1.1 version: 13.1.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -161,6 +164,9 @@ importers: '@vitest/coverage-v8': specifier: 4.1.11 version: 4.1.11(vitest@4.1.11) + beasties: + specifier: ^0.4.3 + version: 0.4.3 drizzle-kit: specifier: 0.31.10 version: 0.31.10 @@ -1934,6 +1940,13 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + beasties@0.4.3: + resolution: {integrity: sha512-fIIeLOcbAB/K1kb1HBVJoiq1alHL4RCYBSo5e7HzrNkkgMggXR1Vqt/Z9JWnkfe/qdCo66Ux3QRwZioAIBdWRA==} + engines: {node: '>=18.0.0'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1985,6 +1998,13 @@ packages: resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} engines: {node: '>=18.0'} + css-select@6.0.0: + resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==} + + css-what@7.0.0: + resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==} + engines: {node: '>= 6'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2019,9 +2039,22 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dompurify@3.4.14: resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==} + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + drizzle-kit@0.31.10: resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} hasBin: true @@ -2128,6 +2161,14 @@ packages: resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -2226,6 +2267,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -2494,6 +2538,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lzma-js@1.0.1: + resolution: {integrity: sha512-JMBqMjHTHvH4ubDADxgJKqDTv9pgKanTDePkxDaMoewY47vevub5O2SCM1f2buXzvppX+FqCY9raJBLMua8PDg==} + lzma@2.3.2: resolution: {integrity: sha512-DcfiawQ1avYbW+hsILhF38IKAlnguc/fjHrychs9hdxe4qLykvhT5VTGNs5YRWgaNePh7NTxGD4uv4gKsRomCQ==} hasBin: true @@ -2619,6 +2666,9 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + oauth4webapi@3.8.7: resolution: {integrity: sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==} @@ -2680,6 +2730,15 @@ packages: postal-mime@2.7.5: resolution: {integrity: sha512-GNEXKvWFQnbgO5NlrGzVa0FmWzBZ24PersAWErttSg1Hjpf0ATxTwS5DOMGaOpTG6bUh5cTr7xi0jAD942wCJA==} + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.5.25 + postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -4295,6 +4354,20 @@ snapshots: baseline-browser-mapping@2.11.16: {} + beasties@0.4.3: + dependencies: + css-select: 6.0.0 + css-what: 7.0.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + htmlparser2: 10.1.0 + picocolors: 1.1.1 + postcss: 8.5.26 + postcss-media-query-parser: 0.2.3 + postcss-safe-parser: 7.0.1(postcss@8.5.26) + + boolbase@1.0.0: {} + buffer-from@1.1.2: {} caniuse-lite@1.0.30001809: {} @@ -4335,6 +4408,16 @@ snapshots: croner@10.0.1: {} + css-select@6.0.0: + dependencies: + boolbase: 1.0.0 + css-what: 7.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@7.0.0: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -4355,10 +4438,28 @@ snapshots: detect-node-es@1.1.0: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + dompurify@3.4.14: optionalDependencies: '@types/trusted-types': 2.0.7 + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + drizzle-kit@0.31.10: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -4381,6 +4482,10 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + entities@4.5.0: {} + + entities@7.0.1: {} + es-module-lexer@2.3.2: {} esbuild@0.25.12: @@ -4487,6 +4592,13 @@ snapshots: html-escaper@2.0.2: {} + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + husky@9.1.7: {} iconv-lite@0.7.3: @@ -4701,6 +4813,8 @@ snapshots: dependencies: react: 19.2.8 + lzma-js@1.0.1: {} + lzma@2.3.2: {} magic-string@0.30.21: @@ -4829,6 +4943,10 @@ snapshots: node-addon-api@7.1.1: {} + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + oauth4webapi@3.8.7: {} obug@2.1.4: {} @@ -4944,6 +5062,12 @@ snapshots: postal-mime@2.7.5: {} + postcss-media-query-parser@0.2.3: {} + + postcss-safe-parser@7.0.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b0781673..293bc9d3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -33,7 +33,7 @@ const pixelify = Pixelify_Sans({ // enforceSiteAccess, DB-backed site settings) to render the element, // which cannot be wrapped in . Child pages inherit this opt-out, // so leaf pages that only access cached/DB data stay instant. -export const instant = false; +// export const instant = false; export async function generateMetadata(): Promise { const appUrl = process.env.APP_URL ?? "http://localhost:3000"; diff --git a/src/lib/admin/photo-files.ts b/src/lib/admin/photo-files.ts index 146b1b43..5941fe2a 100644 --- a/src/lib/admin/photo-files.ts +++ b/src/lib/admin/photo-files.ts @@ -1,5 +1,6 @@ import { unlink } from "node:fs/promises"; import path from "node:path"; +import { getRuntimeResolve } from "@/lib/utils/runtime-path"; /** * Best-effort local file delete for camera photos hosted under /public. @@ -25,8 +26,11 @@ export async function tryRemoveLocalPhotoFile(url: string): Promise { if (!pathname.startsWith("/")) return false; const rel = pathname.replace(/^\/+/, ""); if (!rel || rel.includes("..")) return false; - const publicRoot = path.resolve(process.cwd(), "public"); - const full = path.resolve(publicRoot, rel); + const publicRoot = /*turbopackIgnore: true*/ getRuntimeResolve( + process.cwd(), + "public", + ); + const full = getRuntimeResolve(publicRoot, rel); if (!full.startsWith(publicRoot + path.sep) && full !== publicRoot) { return false; } diff --git a/src/lib/media-storage.ts b/src/lib/media-storage.ts index 944e4dcf..90809697 100644 --- a/src/lib/media-storage.ts +++ b/src/lib/media-storage.ts @@ -1,13 +1,14 @@ import path from "node:path"; +import { getRuntimeResolve } from "@/lib/utils/runtime-path"; /** * Persistent media root. Lives outside `public/` and outside Git so uploads * survive rebuilds, redeploys and `git clean`. Served through /api/media. */ -export const MEDIA_ROOT = path.resolve(process.cwd(), "storage", "media"); +export const MEDIA_ROOT = getRuntimeResolve(process.cwd(), "storage", "media"); export function resolveMediaPath(...segments: string[]): string { - const target = path.resolve(MEDIA_ROOT, ...segments); + const target = getRuntimeResolve(MEDIA_ROOT, ...segments); if (target !== MEDIA_ROOT && !target.startsWith(MEDIA_ROOT + path.sep)) { throw new Error("Invalid media path"); } diff --git a/src/lib/services/catalog-assets.test.ts b/src/lib/services/catalog-assets.test.ts index 628af4fb..d8502707 100644 --- a/src/lib/services/catalog-assets.test.ts +++ b/src/lib/services/catalog-assets.test.ts @@ -1,23 +1,23 @@ -import path from "node:path"; import { describe, expect, it } from "vitest"; import { catalogueAssetDirectories } from "@/lib/services/catalog-assets"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; describe("catalogue asset directories", () => { it("prefers the configured Gamedata catalogue and retains the bundled fallback", () => { - const cwd = path.join("test", "cms"); - const gamedataRoot = path.join("test", "Gamedata"); + const cwd = getRuntimePath("test", "cms"); + const gamedataRoot = getRuntimePath("test", "Gamedata"); expect(catalogueAssetDirectories(gamedataRoot, cwd)).toEqual([ - path.join(gamedataRoot, "c_images", "catalogue"), - path.join(cwd, "public", "swf", "c_images", "catalogue"), + getRuntimePath(gamedataRoot, "c_images", "catalogue"), + getRuntimePath(cwd, "public", "swf", "c_images", "catalogue"), ]); }); it("uses only the bundled catalogue when no Gamedata root is configured", () => { - const cwd = path.join("test", "cms"); + const cwd = getRuntimePath("test", "cms"); expect(catalogueAssetDirectories("", cwd)).toEqual([ - path.join(cwd, "public", "swf", "c_images", "catalogue"), + getRuntimePath(cwd, "public", "swf", "c_images", "catalogue"), ]); }); }); diff --git a/src/lib/services/catalog-assets.ts b/src/lib/services/catalog-assets.ts index 8f527bdb..61b45f99 100644 --- a/src/lib/services/catalog-assets.ts +++ b/src/lib/services/catalog-assets.ts @@ -1,14 +1,14 @@ import { promises as fs } from "node:fs"; -import path from "node:path"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; export function catalogueAssetDirectories( gamedataRoot: string, cwd = process.cwd(), ): string[] { - const bundled = path.join(cwd, "public", "swf", "c_images", "catalogue"); + const bundled = getRuntimePath(cwd, "public", "swf", "c_images", "catalogue"); if (!gamedataRoot.trim()) return [bundled]; - return [path.join(gamedataRoot, "c_images", "catalogue"), bundled]; + return [getRuntimePath(gamedataRoot, "c_images", "catalogue"), bundled]; } export async function readCatalogueAssetFiles( diff --git a/src/lib/services/catalog-audit.ts b/src/lib/services/catalog-audit.ts index 039de870..0ec9922e 100644 --- a/src/lib/services/catalog-audit.ts +++ b/src/lib/services/catalog-audit.ts @@ -1,5 +1,4 @@ import { readdir } from "node:fs/promises"; -import path from "node:path"; import { sql } from "drizzle-orm"; import { db } from "@/lib/db"; import { fetchSourceFurnidata } from "@/lib/services/clone-import"; @@ -8,6 +7,7 @@ import { getFurniAssetDirs, getGamedataRoot, } from "@/lib/services/furni-asset-dirs"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; import { applyCatalogSql, generateCatalogSql, @@ -182,7 +182,7 @@ export async function runCatalogAudit( try { const gamedataRoot = await getGamedataRoot(); if (!gamedataRoot) return [] as string[]; - return await readdir(path.join(gamedataRoot, "album1584")).catch( + return await readdir(getRuntimePath(gamedataRoot, "album1584")).catch( () => [] as string[], ); } catch { diff --git a/src/lib/services/catalog-repair-live.test.ts b/src/lib/services/catalog-repair-live.test.ts index 58e39453..4d6c77c1 100644 --- a/src/lib/services/catalog-repair-live.test.ts +++ b/src/lib/services/catalog-repair-live.test.ts @@ -6,7 +6,6 @@ // FurnitureData.json is only ever read (a copy is made first). import { promises as fs } from "node:fs"; import os from "node:os"; -import path from "node:path"; import { sql } from "drizzle-orm"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { db } from "@/lib/db"; @@ -23,6 +22,7 @@ import { classifyCatalogItem, NO_CATALOG_KINDS, } from "@/lib/services/item-kind"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } = vi.hoisted(() => ({ @@ -59,9 +59,11 @@ let tmpRoot: string; let gamedataDir: string; beforeAll(async () => { - tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "furni-repair-rehearsal-")); - gamedataDir = path.join(tmpRoot, "Gamedata"); - await fs.mkdir(path.join(gamedataDir, "config"), { recursive: true }); + tmpRoot = await fs.mkdtemp( + getRuntimePath(os.tmpdir(), "furni-repair-rehearsal-"), + ); + gamedataDir = getRuntimePath(tmpRoot, "Gamedata"); + await fs.mkdir(getRuntimePath(gamedataDir, "config"), { recursive: true }); getGamedataRootMock.mockResolvedValue(gamedataDir); getNitroFilesRootMock.mockResolvedValue(""); settingGetMock.mockImplementation( @@ -75,7 +77,7 @@ beforeAll(async () => { try { await fs.copyFile( REAL_FURNIDATA, - path.join(gamedataDir, "config", "FurnitureData.json"), + getRuntimePath(gamedataDir, "config", "FurnitureData.json"), ); } catch { // missing copy — repair still runs against an empty file @@ -242,7 +244,7 @@ describeGuard("catalog studio live repair rehearsal (sandbox)", () => { byName.set(r.item_name, (byName.get(r.item_name) ?? 0) + 1); const finalFd = JSON.parse( await fs.readFile( - path.join(gamedataDir, "config", "FurnitureData.json"), + getRuntimePath(gamedataDir, "config", "FurnitureData.json"), "utf8", ), ) as Record> }>; diff --git a/src/lib/services/clone-import.ts b/src/lib/services/clone-import.ts index b1e2b346..37d7627a 100644 --- a/src/lib/services/clone-import.ts +++ b/src/lib/services/clone-import.ts @@ -1,5 +1,4 @@ import { promises as fs } from "node:fs"; -import path from "node:path"; import { eq, inArray, sql } from "drizzle-orm"; import { db, ItemsBase } from "@/lib/db"; import { autoDetectInteraction } from "@/lib/furni/auto-interaction"; @@ -25,6 +24,7 @@ import { import { downloadFile } from "@/lib/services/import/core/download"; import { siteSettings } from "@/lib/services/site-settings"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; export interface SourceFurni { id: number; @@ -287,11 +287,11 @@ export async function cloneSingleFurni(params: { // Download .nitro + icon directly from the source hotel. onProgress?.("downloading"); - const nitroPath = path.join( + const nitroPath = getRuntimePath( /*turbopackIgnore: true*/ nitroDir, `${classname}.nitro`, ); - const iconPath = path.join( + const iconPath = getRuntimePath( /*turbopackIgnore: true*/ iconDir, `${classname}_icon.png`, ); diff --git a/src/lib/services/effect-import.ts b/src/lib/services/effect-import.ts index c7f8bf5a..183cfa9b 100644 --- a/src/lib/services/effect-import.ts +++ b/src/lib/services/effect-import.ts @@ -1,5 +1,4 @@ import { existsSync, promises as fs } from "node:fs"; -import path from "node:path"; import { listOfficialEffects, resolveEffectSwfUrl, @@ -13,17 +12,18 @@ import { getGamedataRoot } from "@/lib/services/furni-asset-dirs"; import { downloadFile } from "@/lib/services/import/core/download"; import { siteSettings } from "@/lib/services/site-settings"; import { convertSwfToNitro } from "@/lib/services/swf-to-nitro"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; import type { EffectListItem, EffectMapEntry, ImportEffectResult, } from "@/types/effects"; -const EFFECT_NITRO_DIR_FALLBACK = path.join( +const EFFECT_NITRO_DIR_FALLBACK = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/nitro-assets/bundled/effect", ); -const EFFECT_SWF_DIR_FALLBACK = path.join( +const EFFECT_SWF_DIR_FALLBACK = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/swf/gordon", ); @@ -35,7 +35,10 @@ async function getEffectNitroDir(): Promise { if (configured) return configured; const gamedataRoot = await getGamedataRoot(); if (gamedataRoot) - return path.join(/*turbopackIgnore: true*/ gamedataRoot, "bundled/effect"); + return getRuntimePath( + /*turbopackIgnore: true*/ gamedataRoot, + "bundled/effect", + ); return EFFECT_NITRO_DIR_FALLBACK; } @@ -61,7 +64,7 @@ export async function ensureEffectDirs(): Promise { } async function nitroPathFor(lib: string): Promise { - return path.join( + return getRuntimePath( /*turbopackIgnore: true*/ await getEffectNitroDir(), `${lib}.nitro`, ); @@ -82,7 +85,7 @@ export async function importSingleEffect(params: { // Download SWF onProgress?.("downloading"); - const swfPath = path.join( + const swfPath = getRuntimePath( /*turbopackIgnore: true*/ await getEffectSwfDir(), `${lib}.swf`, ); @@ -167,7 +170,7 @@ export async function getEffectList(search: string): Promise { ...e, present: localByLib.has(e.lib), nitroExists: existsSync( - /*turbopackIgnore: true*/ path.join(nitroDir, `${e.lib}.nitro`), + /*turbopackIgnore: true*/ getRuntimePath(nitroDir, `${e.lib}.nitro`), ), })); return term diff --git a/src/lib/services/effectmap.test.ts b/src/lib/services/effectmap.test.ts index 9f785f93..dbcf7949 100644 --- a/src/lib/services/effectmap.test.ts +++ b/src/lib/services/effectmap.test.ts @@ -1,7 +1,7 @@ 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 { getRuntimePath } from "@/lib/utils/runtime-path"; import type { EffectMapEntry } from "@/types/effects"; import { __setEffectMapPathForTests, @@ -26,8 +26,8 @@ const ENTRY: EffectMapEntry = { }; beforeEach(async () => { - dir = await fs.mkdtemp(path.join(os.tmpdir(), "effectmap-")); - file = path.join(dir, "EffectMap.json"); + dir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "effectmap-")); + file = getRuntimePath(dir, "EffectMap.json"); await fs.writeFile(file, JSON.stringify({ effects: [ENTRY] })); __setEffectMapPathForTests(file); }); diff --git a/src/lib/services/figure-import.ts b/src/lib/services/figure-import.ts index cc315cbc..cc1d9fb7 100644 --- a/src/lib/services/figure-import.ts +++ b/src/lib/services/figure-import.ts @@ -1,11 +1,11 @@ import { existsSync, promises as fs } from "node:fs"; -import path from "node:path"; import { resolveFigureSwfUrl } from "@/lib/services/figure-source"; import { listFigureLibraries } from "@/lib/services/figuremap"; import { getGamedataRoot } from "@/lib/services/furni-asset-dirs"; import { downloadFile } from "@/lib/services/import/core/download"; import { siteSettings } from "@/lib/services/site-settings"; import { convertSwfToNitro } from "@/lib/services/swf-to-nitro"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; export interface FigureListItem { lib: string; @@ -24,11 +24,11 @@ export interface ImportFigureResult { error?: string; } -const FIGURE_NITRO_DIR_FALLBACK = path.join( +const FIGURE_NITRO_DIR_FALLBACK = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/nitro-assets/bundled/figure", ); -const FIGURE_SWF_DIR_FALLBACK = path.join( +const FIGURE_SWF_DIR_FALLBACK = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/swf/gordon", ); @@ -40,7 +40,10 @@ async function getFigureNitroDir(): Promise { if (configured) return configured; const gamedataRoot = await getGamedataRoot(); if (gamedataRoot) - return path.join(/*turbopackIgnore: true*/ gamedataRoot, "bundled/figure"); + return getRuntimePath( + /*turbopackIgnore: true*/ gamedataRoot, + "bundled/figure", + ); return FIGURE_NITRO_DIR_FALLBACK; } @@ -66,7 +69,7 @@ export async function ensureFigureDirs(): Promise { } async function nitroPathFor(lib: string): Promise { - return path.join( + return getRuntimePath( /*turbopackIgnore: true*/ await getFigureNitroDir(), `${lib}.nitro`, ); @@ -81,7 +84,7 @@ export async function importSingleFigure(params: { await ensureFigureDirs(); onProgress?.("downloading"); - const swfPath = path.join( + const swfPath = getRuntimePath( /*turbopackIgnore: true*/ await getFigureSwfDir(), `${lib}.swf`, ); @@ -158,7 +161,7 @@ export async function getFigureList(params: { lib: l.id, revision: l.revision, nitroExists: existsSync( - /*turbopackIgnore: true*/ path.join(nitroDir, `${l.id}.nitro`), + /*turbopackIgnore: true*/ getRuntimePath(nitroDir, `${l.id}.nitro`), ), previewType: l.parts?.[0]?.type, previewId: l.parts?.[0]?.id, @@ -176,7 +179,9 @@ export async function getFigureStats(): Promise<{ getFigureNitroDir(), ]); const imported = libs.filter((l) => - existsSync(/*turbopackIgnore: true*/ path.join(nitroDir, `${l.id}.nitro`)), + existsSync( + /*turbopackIgnore: true*/ getRuntimePath(nitroDir, `${l.id}.nitro`), + ), ).length; return { total: libs.length, imported, missing: libs.length - imported }; } diff --git a/src/lib/services/figuremap.test.ts b/src/lib/services/figuremap.test.ts index ea145428..eede14fa 100644 --- a/src/lib/services/figuremap.test.ts +++ b/src/lib/services/figuremap.test.ts @@ -1,7 +1,7 @@ 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 { getRuntimePath } from "@/lib/utils/runtime-path"; import type { FigureLibrary } from "./figuremap"; import { __setFigureMapPathForTests, @@ -23,8 +23,8 @@ const LIB: FigureLibrary = { }; beforeEach(async () => { - dir = await fs.mkdtemp(path.join(os.tmpdir(), "figuremap-")); - file = path.join(dir, "FigureMap.json"); + dir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "figuremap-")); + file = getRuntimePath(dir, "FigureMap.json"); await fs.writeFile(file, JSON.stringify({ libraries: [LIB] })); __setFigureMapPathForTests(file); }); diff --git a/src/lib/services/furni-asset-dirs.test.ts b/src/lib/services/furni-asset-dirs.test.ts index b71d9543..73257f10 100644 --- a/src/lib/services/furni-asset-dirs.test.ts +++ b/src/lib/services/furni-asset-dirs.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node -import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; const { existsSyncMock, settingGetMock } = vi.hoisted(() => ({ existsSyncMock: vi.fn(), @@ -19,9 +19,9 @@ vi.mock("@/lib/services/site-settings", () => ({ import { getFurniAssetWriteTargets } from "./furni-asset-dirs"; const primary = { - swfDir: path.join("test-assets", "primary", "swf"), - iconDir: path.join("test-assets", "primary", "icons"), - nitroDir: path.join("test-assets", "primary", "furniture"), + swfDir: getRuntimePath("test-assets", "primary", "swf"), + iconDir: getRuntimePath("test-assets", "primary", "icons"), + nitroDir: getRuntimePath("test-assets", "primary", "furniture"), }; function configureSettings(values: Record): void { @@ -38,7 +38,7 @@ describe("getFurniAssetWriteTargets", () => { }); it("maps a configured production Gamedata root to the directories served by Nitro", async () => { - const gamedataRoot = path.join("test-assets", "Gamedata"); + const gamedataRoot = getRuntimePath("test-assets", "Gamedata"); configureSettings({ furni_swf_dir: primary.swfDir, furni_icon_dir: primary.iconDir, @@ -51,8 +51,8 @@ describe("getFurniAssetWriteTargets", () => { expect(targets.mirrorDirs).toContainEqual({ swfDir: primary.swfDir, - iconDir: path.join(gamedataRoot, "icons"), - nitroDir: path.join(gamedataRoot, "bundled", "furniture"), + iconDir: getRuntimePath(gamedataRoot, "icons"), + nitroDir: getRuntimePath(gamedataRoot, "bundled", "furniture"), }); }); @@ -71,11 +71,11 @@ describe("getFurniAssetWriteTargets", () => { }); it("does not duplicate a Gamedata layout already used as the primary target", async () => { - const gamedataRoot = path.join("test-assets", "Gamedata"); + const gamedataRoot = getRuntimePath("test-assets", "Gamedata"); configureSettings({ furni_swf_dir: primary.swfDir, - furni_icon_dir: path.join(gamedataRoot, "icons"), - furni_nitro_dir: path.join(gamedataRoot, "bundled", "furniture"), + furni_icon_dir: getRuntimePath(gamedataRoot, "icons"), + furni_nitro_dir: getRuntimePath(gamedataRoot, "bundled", "furniture"), nitro_files_root: "", gamedata_root: gamedataRoot, }); diff --git a/src/lib/services/furni-asset-dirs.ts b/src/lib/services/furni-asset-dirs.ts index 76c9515d..4e2906b9 100644 --- a/src/lib/services/furni-asset-dirs.ts +++ b/src/lib/services/furni-asset-dirs.ts @@ -1,23 +1,24 @@ import { existsSync } from "node:fs"; import path from "node:path"; import { siteSettings } from "@/lib/services/site-settings"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; -export const DEFAULT_NITRO_FILES_ROOT = path.join( +export const DEFAULT_NITRO_FILES_ROOT = getRuntimePath( /*turbopackIgnore: true*/ path.dirname(process.cwd()), "DEV", "Nitro-Files", ); export const DEFAULT_GAMEDATA_ROOT = process.platform === "win32" ? "" : "/var/www/Gamedata"; -export const DEFAULT_FURNI_ICON_DIR = path.join( +export const DEFAULT_FURNI_ICON_DIR = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/swf/dcr/hof_furni/icons", ); -export const DEFAULT_FURNI_SWF_DIR = path.join( +export const DEFAULT_FURNI_SWF_DIR = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/swf/dcr/hof_furni/swf", ); -export const DEFAULT_FURNI_NITRO_DIR = path.join( +export const DEFAULT_FURNI_NITRO_DIR = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/nitro-assets/bundled/furniture", ); @@ -77,12 +78,12 @@ export async function getGamedataRoot(): Promise { function dirsFromNitroFilesRoot(root: string): FurniAssetDirs { return { - swfDir: path.join(/*turbopackIgnore: true*/ root, "swf/dcr/hof_furni"), - iconDir: path.join( + swfDir: getRuntimePath(/*turbopackIgnore: true*/ root, "swf/dcr/hof_furni"), + iconDir: getRuntimePath( /*turbopackIgnore: true*/ root, "swf/dcr/hof_furni/icons", ), - nitroDir: path.join( + nitroDir: getRuntimePath( /*turbopackIgnore: true*/ root, "nitro-assets/bundled/furniture", ), @@ -95,8 +96,11 @@ function dirsFromGamedataRoot( ): FurniAssetDirs { return { swfDir: primarySwfDir, - iconDir: path.join(/*turbopackIgnore: true*/ root, "icons"), - nitroDir: path.join(/*turbopackIgnore: true*/ root, "bundled/furniture"), + iconDir: getRuntimePath(/*turbopackIgnore: true*/ root, "icons"), + nitroDir: getRuntimePath( + /*turbopackIgnore: true*/ root, + "bundled/furniture", + ), }; } diff --git a/src/lib/services/furni-data-i18n.ts b/src/lib/services/furni-data-i18n.ts index f8a4cef0..63f36618 100644 --- a/src/lib/services/furni-data-i18n.ts +++ b/src/lib/services/furni-data-i18n.ts @@ -1,3 +1,5 @@ +import path from "node:path"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; /** * Localized furnidata builder — generates FurnitureData_.json files * (nl/en/de/fr/es/tr/it/pt/fi/pl/ru/ar/ja) from the master FurnitureData.json @@ -10,7 +12,6 @@ */ import { existsSync, promises as fs } from "node:fs"; -import path from "node:path"; import type { HabboGamedataHotel } from "@/lib/habbo-gamedata-hotel"; import { habboFurnidataUrl } from "@/lib/habbo-gamedata-hotel"; import { logger } from "@/lib/logger"; @@ -105,8 +106,8 @@ async function loadLibreCache(): Promise> { try { const gamedataRoot = await getGamedataRoot(); const cachePath = gamedataRoot - ? path.join(gamedataRoot, "config", LIBRE_CACHE_FILE) - : path.join(process.cwd(), LIBRE_CACHE_FILE); + ? getRuntimePath(gamedataRoot, "config", LIBRE_CACHE_FILE) + : getRuntimePath(process.cwd(), LIBRE_CACHE_FILE); if (existsSync(cachePath)) { const raw = await fs.readFile(cachePath, "utf-8"); const obj = JSON.parse(raw) as LibreCache; @@ -126,8 +127,8 @@ async function saveLibreCache(): Promise { try { const gamedataRoot = await getGamedataRoot(); const cachePath = gamedataRoot - ? path.join(gamedataRoot, "config", LIBRE_CACHE_FILE) - : path.join(process.cwd(), LIBRE_CACHE_FILE); + ? getRuntimePath(gamedataRoot, "config", LIBRE_CACHE_FILE) + : getRuntimePath(process.cwd(), LIBRE_CACHE_FILE); const obj: LibreCache = Object.fromEntries(libreMemoryCache.entries()); await fs.mkdir(path.dirname(cachePath), { recursive: true }); await fs.writeFile(cachePath, JSON.stringify(obj), "utf-8"); @@ -363,7 +364,7 @@ async function getLocalizedWritePaths(lang: string): Promise { for (const base of bases) { const dir = path.dirname(base); const langFile = `FurnitureData_${lang}.json`; - out.push(path.join(dir, langFile)); + out.push(getRuntimePath(dir, langFile)); } // Deduplicate same normalized path. const seen = new Set(); diff --git a/src/lib/services/furni-data-paths.test.ts b/src/lib/services/furni-data-paths.test.ts index ba951a86..6679fc11 100644 --- a/src/lib/services/furni-data-paths.test.ts +++ b/src/lib/services/furni-data-paths.test.ts @@ -1,8 +1,8 @@ // @vitest-environment node 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 { getRuntimePath } from "@/lib/utils/runtime-path"; const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } = vi.hoisted(() => ({ @@ -26,13 +26,15 @@ describe("writeFurniData production mirror", () => { let tempDir: string; beforeEach(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "furni-data-paths-")); + tempDir = await fs.mkdtemp( + getRuntimePath(os.tmpdir(), "furni-data-paths-"), + ); getNitroFilesRootMock.mockResolvedValue(""); - getGamedataRootMock.mockResolvedValue(path.join(tempDir, "Gamedata")); + getGamedataRootMock.mockResolvedValue(getRuntimePath(tempDir, "Gamedata")); settingGetMock.mockImplementation( async (key: string, fallback: string | null = null) => { if (key === "furni_data_url") { - return path.join(tempDir, "cms", "FurnitureData.json"); + return getRuntimePath(tempDir, "cms", "FurnitureData.json"); } if (key === "furni_data_mirror_path") return ""; return fallback; @@ -50,7 +52,7 @@ describe("writeFurniData production mirror", () => { await writeFurniData(data); - const livePath = path.join( + const livePath = getRuntimePath( tempDir, "Gamedata", "config", @@ -67,7 +69,7 @@ describe("writeFurniData production mirror", () => { ); await expect(getFurnitureDataPath()).resolves.toBe( - path.join(tempDir, "Gamedata", "config", "FurnitureData.json"), + getRuntimePath(tempDir, "Gamedata", "config", "FurnitureData.json"), ); }); }); diff --git a/src/lib/services/furni-data.ts b/src/lib/services/furni-data.ts index b6c48245..1b3378f4 100644 --- a/src/lib/services/furni-data.ts +++ b/src/lib/services/furni-data.ts @@ -9,6 +9,7 @@ import { import type { OfficialHabboFurniEntry } from "@/lib/services/habbo-furnidata-cache"; import { siteSettings } from "@/lib/services/site-settings"; import type { FurniMetadata } from "@/lib/services/swf-to-nitro"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; const LOCK_STALE_MS = 60_000; const DEFAULT_FURNI_DATA_URL = "/gamedata/config/FurnitureData.json"; @@ -27,14 +28,18 @@ export async function getFurnitureDataPath(): Promise { const gamedataRoot = await getGamedataRoot(); if (gamedataRoot) { const relative = url.replace(/^\/gamedata\/?/, ""); - return path.join(/*turbopackIgnore: true*/ gamedataRoot, relative); + return getRuntimePath(/*turbopackIgnore: true*/ gamedataRoot, relative); } } const relative = url.replace(/^\/+/, ""); // The `relative` segment is admin-configurable so its concrete value is // unknown at build time; without the hint Turbopack traces the entire // public/ tree (~400k files) into the route's NFT. - return path.join(/*turbopackIgnore: true*/ process.cwd(), "public", relative); + return getRuntimePath( + /*turbopackIgnore: true*/ process.cwd(), + "public", + relative, + ); } export async function getFurnitureDataWritePaths(): Promise { @@ -48,14 +53,14 @@ export async function getFurnitureDataWritePaths(): Promise { const mirror = configuredMirror.trim() ? configuredMirror : nitroFilesRoot - ? path.join( + ? getRuntimePath( /*turbopackIgnore: true*/ nitroFilesRoot, "nitro-assets/gamedata/FurnitureData.json", ) : ""; const gamedataPath = gamedataRoot - ? path.join( + ? getRuntimePath( /*turbopackIgnore: true*/ gamedataRoot, "config/FurnitureData.json", ) diff --git a/src/lib/services/furni-import.ts b/src/lib/services/furni-import.ts index bcb1deee..f97d3a5e 100644 --- a/src/lib/services/furni-import.ts +++ b/src/lib/services/furni-import.ts @@ -38,6 +38,7 @@ import { extractIconFromSwfAsync, parseNitroBundle, } from "@/lib/services/swf-to-nitro"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; import type { ImportSingleResult } from "@/types/furni"; // Re-export the download helpers now owned by the shared import core. @@ -95,7 +96,7 @@ async function mirrorFileToDirs( if (!existsSync(/*turbopackIgnore: true*/ sourcePath)) return; for (const dir of dirs) { - const targetPath = path.join(/*turbopackIgnore: true*/ dir, fileName); + const targetPath = getRuntimePath(/*turbopackIgnore: true*/ dir, fileName); const sourceKey = process.platform === "win32" ? sourcePath.toLowerCase() : sourcePath; const targetKey = @@ -362,10 +363,10 @@ async function copyIfMissing( entry: string, copied: string[], ): Promise { - const dest = path.join(targetDir, entry); + const dest = getRuntimePath(targetDir, entry); if (existsSync(dest)) return; try { - await fs.copyFile(path.join(sourceDir, entry), dest); + await fs.copyFile(getRuntimePath(sourceDir, entry), dest); copied.push(entry); } catch (err) { logger.warn("[import-furni] gamedata bundle sync copy failed", { @@ -423,7 +424,10 @@ export async function syncAssetsToGamedataBundle(): Promise<{ const copiedNitros: string[] = []; const copiedIcons: string[] = []; - const nitroTarget = path.join(gamedataRoot, "bundled/furniture"); + const nitroTarget = /*turbopackIgnore: true*/ getRuntimePath( + gamedataRoot, + "bundled/furniture", + ); if (nitroDir !== nitroTarget) { const copied = await syncDirMissingFiles(nitroDir, nitroTarget, (entry) => entry.endsWith(".nitro"), @@ -431,7 +435,10 @@ export async function syncAssetsToGamedataBundle(): Promise<{ copiedNitros.push(...copied); } - const iconTarget = path.join(gamedataRoot, "icons"); + const iconTarget = /*turbopackIgnore: true*/ getRuntimePath( + gamedataRoot, + "icons", + ); if (iconDir !== iconTarget) { const copied = await syncDirMissingFiles(iconDir, iconTarget, (entry) => entry.endsWith("_icon.png"), @@ -680,15 +687,15 @@ export async function importSingleFurni(params: { const iconFileName = classname.replace(/\*/g, "_"); const safeSwfName = baseClassname; const safeNitroName = baseClassname; - const swfPath = path.join( + const swfPath = getRuntimePath( /*turbopackIgnore: true*/ swfDir, `${safeSwfName}.swf`, ); - const nitroPath = path.join( + const nitroPath = getRuntimePath( /*turbopackIgnore: true*/ nitroDir, `${safeNitroName}.nitro`, ); - const iconPath = path.join( + const iconPath = getRuntimePath( /*turbopackIgnore: true*/ iconDir, `${iconFileName}_icon.png`, ); @@ -1092,7 +1099,7 @@ async function realNitroModes(classname: string): Promise { const cached = nitroModesCache.get(base); if (cached !== undefined) return cached; try { - const nitroPath = path.join( + const nitroPath = getRuntimePath( /*turbopackIgnore: true*/ FURNI_NITRO_DIR, `${base}.nitro`, ); @@ -1317,16 +1324,25 @@ export async function deleteImportedItem(classname: string): Promise<{ // 4. Delete files (.nitro, .swf, icon) const filesToDelete = [ - path.join(/*turbopackIgnore: true*/ nitroDir, `${baseClassname}.nitro`), - path.join(/*turbopackIgnore: true*/ swfDir, `${baseClassname}.swf`), - path.join(/*turbopackIgnore: true*/ iconDir, `${iconFileName}_icon.png`), + getRuntimePath( + /*turbopackIgnore: true*/ nitroDir, + `${baseClassname}.nitro`, + ), + getRuntimePath(/*turbopackIgnore: true*/ swfDir, `${baseClassname}.swf`), + getRuntimePath( + /*turbopackIgnore: true*/ iconDir, + `${iconFileName}_icon.png`, + ), ...assetTargets.mirrorDirs.flatMap((dirs) => [ - path.join( + getRuntimePath( /*turbopackIgnore: true*/ dirs.nitroDir, `${baseClassname}.nitro`, ), - path.join(/*turbopackIgnore: true*/ dirs.swfDir, `${baseClassname}.swf`), - path.join( + getRuntimePath( + /*turbopackIgnore: true*/ dirs.swfDir, + `${baseClassname}.swf`, + ), + getRuntimePath( /*turbopackIgnore: true*/ dirs.iconDir, `${iconFileName}_icon.png`, ), @@ -1366,14 +1382,16 @@ export async function cleanupConvertedSwfs(): Promise<{ for (const swfFile of swfFiles) { const cn = swfFile.replace(".swf", ""); - const nitroFilePath = path.join( + const nitroFilePath = getRuntimePath( /*turbopackIgnore: true*/ nitroDir, `${cn}.nitro`, ); if (existsSync(/*turbopackIgnore: true*/ nitroFilePath)) { try { - await fs.unlink(path.join(/*turbopackIgnore: true*/ swfDir, swfFile)); + await fs.unlink( + getRuntimePath(/*turbopackIgnore: true*/ swfDir, swfFile), + ); deleted++; } catch (err) { errors.push(`Failed to delete ${swfFile}: ${(err as Error).message}`); @@ -1513,13 +1531,16 @@ export async function rollbackFurniImport( // 4. Delete asset files const filesToDelete = [ - path.join(nitroDir, `${base}.nitro`), - path.join(swfDir, `${base}.swf`), - path.join(iconDir, `${classname.replace(/\*/g, "_")}_icon.png`), + /*turbopackIgnore: true*/ getRuntimePath(nitroDir, `${base}.nitro`), + getRuntimePath(swfDir, `${base}.swf`), + /*turbopackIgnore: true*/ getRuntimePath( + iconDir, + `${classname.replace(/\*/g, "_")}_icon.png`, + ), ]; for (const filePath of filesToDelete) { - if (existsSync(filePath)) { + if (existsSync(/*turbopackIgnore: true*/ filePath)) { try { await fs.unlink(filePath); } catch (err) { @@ -1533,11 +1554,17 @@ export async function rollbackFurniImport( // Mirror directories for (const dirs of assetTargets.mirrorDirs) { for (const filePath of [ - path.join(dirs.nitroDir, `${classname.split("*")[0]}.nitro`), - path.join(dirs.swfDir, `${classname.split("*")[0]}.swf`), - path.join(dirs.iconDir, `${classname.replace(/\*/g, "_")}_icon.png`), + /*turbopackIgnore: true*/ getRuntimePath( + dirs.nitroDir, + `${classname.split("*")[0]}.nitro`, + ), + getRuntimePath(dirs.swfDir, `${classname.split("*")[0]}.swf`), + /*turbopackIgnore: true*/ getRuntimePath( + dirs.iconDir, + `${classname.replace(/\*/g, "_")}_icon.png`, + ), ]) { - if (existsSync(filePath)) { + if (existsSync(/*turbopackIgnore: true*/ filePath)) { try { await fs.unlink(filePath); } catch { diff --git a/src/lib/services/import-badge.ts b/src/lib/services/import-badge.ts index 6a58d315..4b649f09 100644 --- a/src/lib/services/import-badge.ts +++ b/src/lib/services/import-badge.ts @@ -2,13 +2,14 @@ import { promises as fs } from "node:fs"; import path from "node:path"; import { db, WebsiteBadges } from "@/lib/db"; import { getGamedataRoot } from "@/lib/services/furni-asset-dirs"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; export type ImportBadgeResult = | { ok: true } | { ok: false; error: string; status: number }; function externalTextsPath(gamedataRoot: string): string { - return path.join(gamedataRoot, "config", "ExternalTexts.json"); + return getRuntimePath(gamedataRoot, "config", "ExternalTexts.json"); } /** Write badge name/desc into ExternalTexts.json (409 if already present). */ diff --git a/src/lib/services/import-live.test.ts b/src/lib/services/import-live.test.ts index c280d2f7..21929534 100644 --- a/src/lib/services/import-live.test.ts +++ b/src/lib/services/import-live.test.ts @@ -7,8 +7,8 @@ // production FurnitureData.json, so production files are never touched. import { promises as fs } from "node:fs"; import os from "node:os"; -import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; const { getGamedataRootMock, getNitroFilesRootMock, settingGetMock } = vi.hoisted(() => ({ @@ -126,9 +126,9 @@ beforeAll(async () => { // ignore cleanup failures; the assertions will surface real problems } - tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "furni-live-audit-")); - gamedataDir = path.join(tmpRoot, "Gamedata"); - await fs.mkdir(path.join(gamedataDir, "config"), { recursive: true }); + tmpRoot = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "furni-live-audit-")); + gamedataDir = getRuntimePath(tmpRoot, "Gamedata"); + await fs.mkdir(getRuntimePath(gamedataDir, "config"), { recursive: true }); getGamedataRootMock.mockResolvedValue(gamedataDir); getNitroFilesRootMock.mockResolvedValue(""); settingGetMock.mockImplementation( @@ -146,7 +146,7 @@ beforeAll(async () => { try { await fs.copyFile( productionFile, - path.join(gamedataDir, "config", "FurnitureData.json"), + getRuntimePath(gamedataDir, "config", "FurnitureData.json"), ); } catch { // file unavailable — reconcile test still runs against an empty file diff --git a/src/lib/services/import/core/asset-paths.ts b/src/lib/services/import/core/asset-paths.ts index 2784b56a..6e278475 100644 --- a/src/lib/services/import/core/asset-paths.ts +++ b/src/lib/services/import/core/asset-paths.ts @@ -1,6 +1,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; import { getGamedataRoot } from "@/lib/services/furni-asset-dirs"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; // The CMS default gamedata JSON paths point under public/nitro-assets/gamedata/, // but this deployment keeps the gamedata JSON (FigureMap/FigureData/EffectMap…) @@ -11,7 +12,11 @@ import { getGamedataRoot } from "@/lib/services/furni-asset-dirs"; // `bundled/` dir, the location the Nitro client loads from.) function pub(...segs: string[]): string { - return path.join(/*turbopackIgnore: true*/ process.cwd(), "public", ...segs); + return getRuntimePath( + /*turbopackIgnore: true*/ process.cwd(), + "public", + ...segs, + ); } /** @@ -41,7 +46,7 @@ export async function resolveGamedataFile( // Deployment gamedata root (e.g. /var/www/Gamedata/config/…). const gamedataRoot = await getGamedataRoot(); if (gamedataRoot) { - const rootConfig = path.join( + const rootConfig = getRuntimePath( /*turbopackIgnore: true*/ gamedataRoot, "config", fallbackName, diff --git a/src/lib/services/import/core/gamedata-json.test.ts b/src/lib/services/import/core/gamedata-json.test.ts index 910f669f..a061e470 100644 --- a/src/lib/services/import/core/gamedata-json.test.ts +++ b/src/lib/services/import/core/gamedata-json.test.ts @@ -1,7 +1,7 @@ import { promises as fs } from "node:fs"; import os from "node:os"; -import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; import { readGamedataJson, withGamedataLock, @@ -12,8 +12,8 @@ let dir: string; let file: string; beforeEach(async () => { - dir = await fs.mkdtemp(path.join(os.tmpdir(), "gamedata-")); - file = path.join(dir, "EffectMap.json"); + dir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "gamedata-")); + file = getRuntimePath(dir, "EffectMap.json"); await fs.writeFile( file, JSON.stringify({ effects: [{ id: "1", lib: "A" }] }), diff --git a/src/lib/services/pet-icon.ts b/src/lib/services/pet-icon.ts index 9e07d0f2..115c467a 100644 --- a/src/lib/services/pet-icon.ts +++ b/src/lib/services/pet-icon.ts @@ -1,15 +1,15 @@ import { existsSync, readFileSync } from "node:fs"; -import path from "node:path"; import { cropRgba, decodePng } from "@/lib/services/imager/png-decode"; import { encodePng, parseNitroBundle } from "@/lib/services/swf/nitro-builder"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; // Pet .nitro can live in the CMS import target or the bundled asset set. const PET_DIRS = [ - path.join( + getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/nitro-assets/bundled/pet", ), - path.join( + getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/Gamedata/bundled/pet", ), @@ -54,7 +54,7 @@ const iconCache = new Map(); function findNitro(lib: string): string | null { for (const dir of PET_DIRS) { - const p = path.join(/*turbopackIgnore: true*/ dir, `${lib}.nitro`); + const p = getRuntimePath(/*turbopackIgnore: true*/ dir, `${lib}.nitro`); if (existsSync(/*turbopackIgnore: true*/ p)) return p; } return null; diff --git a/src/lib/services/pet-import.ts b/src/lib/services/pet-import.ts index 980cabb2..87765082 100644 --- a/src/lib/services/pet-import.ts +++ b/src/lib/services/pet-import.ts @@ -1,8 +1,8 @@ import { existsSync, promises as fs } from "node:fs"; -import path from "node:path"; import { downloadFile } from "@/lib/services/import/core/download"; import { CANONICAL_PETS, resolvePetSwfUrl } from "@/lib/services/pet-source"; import { convertSwfToNitro } from "@/lib/services/swf-to-nitro"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; export interface PetListItem { lib: string; @@ -16,11 +16,11 @@ export interface ImportPetResult { error?: string; } -export const PET_NITRO_DIR = path.join( +export const PET_NITRO_DIR = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/nitro-assets/bundled/pet", ); -export const PET_SWF_DIR = path.join( +export const PET_SWF_DIR = getRuntimePath( /*turbopackIgnore: true*/ process.cwd(), "public/swf/gordon", ); @@ -36,7 +36,10 @@ export async function ensurePetDirs(): Promise { } function nitroPathFor(lib: string): string { - return path.join(/*turbopackIgnore: true*/ PET_NITRO_DIR, `${lib}.nitro`); + return getRuntimePath( + /*turbopackIgnore: true*/ PET_NITRO_DIR, + `${lib}.nitro`, + ); } export async function importSinglePet(params: { @@ -48,7 +51,7 @@ export async function importSinglePet(params: { await ensurePetDirs(); onProgress?.("downloading"); - const swfPath = path.join( + const swfPath = getRuntimePath( /*turbopackIgnore: true*/ PET_SWF_DIR, `${lib}.swf`, ); diff --git a/src/lib/services/repair-icons.ts b/src/lib/services/repair-icons.ts index 25ef227b..22319a03 100644 --- a/src/lib/services/repair-icons.ts +++ b/src/lib/services/repair-icons.ts @@ -1,6 +1,5 @@ import { existsSync, promises as fs } from "node:fs"; import os from "node:os"; -import path from "node:path"; import { sql } from "drizzle-orm"; import { db } from "@/lib/db"; import { extractFurniIconPng } from "@/lib/services/clone-icon"; @@ -16,6 +15,7 @@ import { } from "@/lib/services/furni-asset-dirs"; import { downloadFile } from "@/lib/services/import/core/download"; import { classifyCatalogItem, NO_ASSET_KINDS } from "@/lib/services/item-kind"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; import { ensureDirectories } from "./furni-import"; const REPAIR_CONCURRENCY = 6; @@ -61,7 +61,7 @@ async function writeIconToDirs( for (const dir of dirs) { try { await fs.mkdir(dir, { recursive: true }); - await fs.writeFile(path.join(dir, fileName), icon); + await fs.writeFile(getRuntimePath(dir, fileName), icon); } catch (err) { errors.push(`write icon to ${dir} failed: ${(err as Error).message}`); } @@ -75,12 +75,12 @@ async function copyIconAcrossDirs( fileName: string, ): Promise { const errors: string[] = []; - const src = path.join(srcDir, fileName); + const src = getRuntimePath(srcDir, fileName); for (const dir of destDirs) { if (dir === srcDir) continue; try { await fs.mkdir(dir, { recursive: true }); - await fs.copyFile(src, path.join(dir, fileName)); + await fs.copyFile(src, getRuntimePath(dir, fileName)); } catch (err) { errors.push(`copy icon to ${dir} failed: ${(err as Error).message}`); } @@ -151,7 +151,7 @@ export async function repairMissingIcons( const gamedataRoot = await getGamedataRoot(); if (gamedataRoot) { const badgeFiles = await fs - .readdir(path.join(gamedataRoot, "album1584")) + .readdir(getRuntimePath(gamedataRoot, "album1584")) .catch(() => [] as string[]); const albumBadgeSet = new Set( badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)), @@ -214,7 +214,7 @@ export async function repairMissingIcons( // Badge items store icons as badge__icon.png (the classname already // includes the `badge_` prefix, so fileName matches correctly). const presentDirs = iconDirs.filter((dir) => - existsSync(path.join(dir, fileName)), + existsSync(getRuntimePath(dir, fileName)), ); const missingDirs = iconDirs.filter((dir) => !presentDirs.includes(dir)); @@ -247,7 +247,7 @@ export async function repairMissingIcons( if (!ok) { let foundNitro = false; for (const dir of nitroDirs) { - const nitroPath = path.join(dir, `${classname}.nitro`); + const nitroPath = getRuntimePath(dir, `${classname}.nitro`); if (!existsSync(nitroPath)) continue; foundNitro = true; try { @@ -308,12 +308,18 @@ export async function repairMissingIcons( try { const dl = await downloadFile( `${source.iconBaseUrl}/${iconName}`, - path.join(targets.iconDir, fileName), + /*turbopackIgnore: true*/ getRuntimePath( + targets.iconDir, + fileName, + ), { maxRetries: 1, validate: "png" }, ); if (dl.ok) { const icon = await fs.readFile( - path.join(targets.iconDir, fileName), + /*turbopackIgnore: true*/ getRuntimePath( + targets.iconDir, + fileName, + ), ); const writeErrors = await writeIconToDirs( icon, @@ -336,7 +342,10 @@ export async function repairMissingIcons( // Some sources only ship icons embedded in their `.nitro` bundles — // grab the bundle and extract the catalog icon from it. if (source.nitroBaseUrl) { - const nitroTmp = path.join(os.tmpdir(), `.repair_${classname}.nitro`); + const nitroTmp = getRuntimePath( + os.tmpdir(), + `.repair_${classname}.nitro`, + ); try { const dl = await downloadFile( `${source.nitroBaseUrl}/${classname}.nitro`, diff --git a/src/lib/services/repair-nitros.ts b/src/lib/services/repair-nitros.ts index 27137f98..7e2f7aaf 100644 --- a/src/lib/services/repair-nitros.ts +++ b/src/lib/services/repair-nitros.ts @@ -1,6 +1,5 @@ import { existsSync, promises as fs } from "node:fs"; import os from "node:os"; -import path from "node:path"; import { sql } from "drizzle-orm"; import { db } from "@/lib/db"; import { @@ -16,6 +15,7 @@ import { import { downloadFile } from "@/lib/services/import/core/download"; import { classifyCatalogItem, NO_ASSET_KINDS } from "@/lib/services/item-kind"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; import { ensureDirectories } from "./furni-import"; import { convertSwfToNitro } from "./swf"; import { addSize32ToBundle } from "./swf/add-size32"; @@ -63,7 +63,7 @@ async function writeNitroToDirs( for (const dir of dirs) { try { await fs.mkdir(dir, { recursive: true }); - await fs.writeFile(path.join(dir, fileName), nitro); + await fs.writeFile(getRuntimePath(dir, fileName), nitro); } catch (err) { errors.push(`write .nitro to ${dir} failed: ${(err as Error).message}`); } @@ -77,12 +77,12 @@ async function copyNitroAcrossDirs( fileName: string, ): Promise { const errors: string[] = []; - const src = path.join(srcDir, fileName); + const src = getRuntimePath(srcDir, fileName); for (const dir of destDirs) { if (dir === srcDir) continue; try { await fs.mkdir(dir, { recursive: true }); - await fs.copyFile(src, path.join(dir, fileName)); + await fs.copyFile(src, getRuntimePath(dir, fileName)); } catch (err) { errors.push(`copy .nitro to ${dir} failed: ${(err as Error).message}`); } @@ -150,7 +150,7 @@ export async function repairMissingNitros( const gamedataRoot = await getGamedataRoot(); if (gamedataRoot) { const badgeFiles = await fs - .readdir(path.join(gamedataRoot, "album1584")) + .readdir(getRuntimePath(gamedataRoot, "album1584")) .catch(() => [] as string[]); albumBadgeSet = new Set( badgeFiles.filter((f) => f.endsWith(".gif")).map((f) => f.slice(0, -4)), @@ -210,7 +210,7 @@ export async function repairMissingNitros( const fileName = `${classname}.nitro`; const presentDirs = nitroDirs.filter((dir) => - existsSync(path.join(dir, fileName)), + existsSync(getRuntimePath(dir, fileName)), ); const missingDirs = nitroDirs.filter((dir) => !presentDirs.includes(dir)); @@ -269,7 +269,7 @@ export async function repairMissingNitros( for (const source of sources) { for (const nitroName of nitroCandidates) { - const tmpPath = path.join( + const tmpPath = getRuntimePath( os.tmpdir(), `.repair_nitro_${classname}_${Math.random().toString(36).slice(2, 8)}.nitro`, ); @@ -469,7 +469,7 @@ export async function reconvertAllNitros( const basesToProcess: string[] = []; for (const base of existingBases) { - if (existsSync(path.join(targets.swfDir, `${base}.swf`))) { + if (existsSync(getRuntimePath(targets.swfDir, `${base}.swf`))) { basesToProcess.push(base); } } @@ -492,7 +492,7 @@ export async function reconvertAllNitros( return; } - const swfPath = path.join(targets.swfDir, `${base}.swf`); + const swfPath = getRuntimePath(targets.swfDir, `${base}.swf`); let swf: Buffer; try { swf = await fs.readFile(swfPath); @@ -636,7 +636,7 @@ export async function addSize32AllNitros( let buf: Buffer | null = null; for (const dir of nitroDirs) { try { - buf = await fs.readFile(path.join(dir, fileName)); + buf = await fs.readFile(getRuntimePath(dir, fileName)); break; } catch { /* try next dir */ diff --git a/src/lib/services/soundtracks.ts b/src/lib/services/soundtracks.ts index c38a91c5..6707b97c 100644 --- a/src/lib/services/soundtracks.ts +++ b/src/lib/services/soundtracks.ts @@ -14,14 +14,14 @@ * `soundtrack_base_url` — see site-settings.ts defaults. */ import { promises as fs } from "node:fs"; -import path from "node:path"; import { parseBuffer } from "music-metadata"; +import { getRuntimePath } from "@/lib/utils/runtime-path"; // ── Constants ────────────────────────────────────────────────────── /** On-disk directory where MP3s live. Fixed (not configurable from UI to * avoid any path traversal risk). */ -export const SOUNDTRACK_DIR = path.join( +export const SOUNDTRACK_DIR = getRuntimePath( process.cwd(), "public", "swf", @@ -48,7 +48,7 @@ export function getSoundtrackPath(code: string): string { if (!isSafeSoundtrackCode(code)) { throw new Error(`Unsafe soundtrack code: ${code}`); } - return path.join(SOUNDTRACK_DIR, `${code}.mp3`); + return getRuntimePath(SOUNDTRACK_DIR, `${code}.mp3`); } /** Ensure the sounds directory exists (mkdir -p). */ diff --git a/src/lib/services/swf/swf-parser.ts b/src/lib/services/swf/swf-parser.ts index 8c52a403..8a640b7e 100644 --- a/src/lib/services/swf/swf-parser.ts +++ b/src/lib/services/swf/swf-parser.ts @@ -3,7 +3,8 @@ */ import { inflateSync } from "node:zlib"; -import LZMA from "lzma"; + +const LZMA = require("lzma/src/lzma_worker").LZMA_WORKER; // ── SWF Tag Types ────────────────────────────────────────────────── export const TAG_END = 0; diff --git a/src/lib/services/upload-import.test.ts b/src/lib/services/upload-import.test.ts index 0f9a179e..720730ac 100644 --- a/src/lib/services/upload-import.test.ts +++ b/src/lib/services/upload-import.test.ts @@ -1,8 +1,8 @@ // @vitest-environment node 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 { getRuntimePath } from "@/lib/utils/runtime-path"; const { appendFurniEntryMock, @@ -96,15 +96,15 @@ describe("uploadSingleFurni live asset mirrors", () => { beforeEach(async () => { __resetUploadItemsBaseAllocatorForTests(); - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upload-furni-")); - assetTargets.swfDir = path.join(tempDir, "cms", "swf"); - assetTargets.iconDir = path.join(tempDir, "cms", "icons"); - assetTargets.nitroDir = path.join(tempDir, "cms", "furniture"); + tempDir = await fs.mkdtemp(getRuntimePath(os.tmpdir(), "upload-furni-")); + assetTargets.swfDir = getRuntimePath(tempDir, "cms", "swf"); + assetTargets.iconDir = getRuntimePath(tempDir, "cms", "icons"); + assetTargets.nitroDir = getRuntimePath(tempDir, "cms", "furniture"); assetTargets.mirrorDirs = [ { swfDir: assetTargets.swfDir, - iconDir: path.join(tempDir, "Gamedata", "icons"), - nitroDir: path.join(tempDir, "Gamedata", "bundled", "furniture"), + iconDir: getRuntimePath(tempDir, "Gamedata", "icons"), + nitroDir: getRuntimePath(tempDir, "Gamedata", "bundled", "furniture"), }, ]; dbExecuteMock.mockReset(); @@ -135,12 +135,12 @@ describe("uploadSingleFurni live asset mirrors", () => { expect(result.ok).toBe(true); await expect( fs.readFile( - path.join(assetTargets.mirrorDirs[0].nitroDir, "chair.nitro"), + getRuntimePath(assetTargets.mirrorDirs[0].nitroDir, "chair.nitro"), ), ).resolves.toEqual(nitroBuffer); await expect( fs.readFile( - path.join(assetTargets.mirrorDirs[0].iconDir, "chair_icon.png"), + getRuntimePath(assetTargets.mirrorDirs[0].iconDir, "chair_icon.png"), ), ).resolves.toEqual(iconBuffer); }); diff --git a/src/lib/services/upload-import.ts b/src/lib/services/upload-import.ts index e4794bf8..10170dc6 100644 --- a/src/lib/services/upload-import.ts +++ b/src/lib/services/upload-import.ts @@ -17,6 +17,7 @@ import { } from "@/lib/services/furni-import"; import { nitroAnimationStatesCount } from "@/lib/services/furni-real-interaction"; import { parseNitroBundle } from "@/lib/services/swf/nitro-builder"; +import { getRuntimePath, getRuntimeResolve } from "@/lib/utils/runtime-path"; export interface UploadResult { ok: boolean; @@ -42,7 +43,7 @@ export interface UploadParams { customparams?: string; } -const MIGRATIONS_DIR = path.resolve( +const MIGRATIONS_DIR = getRuntimeResolve( /*turbopackIgnore: true*/ process.cwd(), "drizzle/migrations", ); @@ -67,7 +68,7 @@ async function mirrorUploadedAsset( const sourceKey = pathKey(sourcePath); const seen = new Set(); for (const dir of dirs) { - const targetPath = path.join(/*turbopackIgnore: true*/ dir, fileName); + const targetPath = getRuntimePath(/*turbopackIgnore: true*/ dir, fileName); const targetKey = pathKey(targetPath); if (targetKey === sourceKey || seen.has(targetKey)) continue; seen.add(targetKey); @@ -166,7 +167,7 @@ async function writeSqlMigration(params: { const padded = String(num).padStart(4, "0"); const className = params.classname.replace(/[^a-zA-Z0-9_-]/g, "_"); const fileName = `${padded}_import_furni_${className}.sql`; - const filePath = path.join( + const filePath = getRuntimePath( /*turbopackIgnore: true*/ MIGRATIONS_DIR, fileName, ); @@ -283,7 +284,7 @@ export async function uploadSingleFurni(params: { const { iconDir, nitroDir } = assetTargets; const nitroFileName = `${classname}.nitro`; - const nitroPath = path.join( + const nitroPath = getRuntimePath( /*turbopackIgnore: true*/ nitroDir, nitroFileName, ); @@ -301,7 +302,7 @@ export async function uploadSingleFurni(params: { let iconPath: string | null = null; if (iconBuffer && iconBuffer.length > 0) { iconFileName = `${classname}_icon.png`; - iconPath = path.join(/*turbopackIgnore: true*/ iconDir, iconFileName); + iconPath = getRuntimePath(/*turbopackIgnore: true*/ iconDir, iconFileName); await fs.writeFile(iconPath, iconBuffer); } diff --git a/src/lib/utils/runtime-path.ts b/src/lib/utils/runtime-path.ts new file mode 100644 index 00000000..2871b97f --- /dev/null +++ b/src/lib/utils/runtime-path.ts @@ -0,0 +1,11 @@ +import path from "path"; + +export function getRuntimePath(...segments: string[]): string { + const resolvePath = new Function("p", "segs", "return p.join(...segs)"); + return resolvePath(path, segments); +} + +export function getRuntimeResolve(...segments: string[]): string { + const resolvePath = new Function("p", "segs", "return p.resolve(...segs)"); + return resolvePath(path, segments); +} diff --git a/src/types/lzma.d.ts b/src/types/lzma.d.ts index 09f2644c..502b42f8 100644 --- a/src/types/lzma.d.ts +++ b/src/types/lzma.d.ts @@ -1,7 +1,14 @@ declare module "lzma" { - const lzma: { - decompress(input: Buffer | Uint8Array | number[]): number[]; - compress(input: string | Uint8Array | number[], mode?: number): number[]; - }; - export default lzma; + export const LZMA: any; + export function decompress( + byte_array: any, + on_finish?: (result: any, error: any) => void, + on_progress?: (percent: number) => void, + ): any; + export function compress( + string: string | Uint8Array, + mode: number, + on_finish?: (result: any, error: any) => void, + on_progress?: (percent: number) => void, + ): any; }