Fix Turbopack module resolution for lzma and restore path imports
This commit is contained in:
1 parent
a5044c80d7
commit
e06596391e
35 files changed
+392
-333
No files matched your search
+2
-161
@@ -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;
|
||||
+3
-1
@@ -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",
|
||||
|
||||
Generated
+125
-1
@@ -83,8 +83,11 @@ importers:
|
||||
specifier: 1.33.0
|
||||
version: 1.33.0([email protected])
|
||||
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([email protected]([email protected]))([email protected])
|
||||
@@ -161,6 +164,9 @@ importers:
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 4.1.11
|
||||
version: 4.1.11([email protected])
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-fIIeLOcbAB/K1kb1HBVJoiq1alHL4RCYBSo5e7HzrNkkgMggXR1Vqt/Z9JWnkfe/qdCo66Ux3QRwZioAIBdWRA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -1985,6 +1998,13 @@ packages:
|
||||
resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==}
|
||||
engines: {node: '>=18.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2019,9 +2039,22 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
|
||||
hasBin: true
|
||||
@@ -2128,6 +2161,14 @@ packages:
|
||||
resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
||||
|
||||
@@ -2226,6 +2267,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-JMBqMjHTHvH4ubDADxgJKqDTv9pgKanTDePkxDaMoewY47vevub5O2SCM1f2buXzvppX+FqCY9raJBLMua8PDg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-DcfiawQ1avYbW+hsILhF38IKAlnguc/fjHrychs9hdxe4qLykvhT5VTGNs5YRWgaNePh7NTxGD4uv4gKsRomCQ==}
|
||||
hasBin: true
|
||||
@@ -2619,6 +2666,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==}
|
||||
|
||||
@@ -2680,6 +2730,15 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-GNEXKvWFQnbgO5NlrGzVa0FmWzBZ24PersAWErttSg1Hjpf0ATxTwS5DOMGaOpTG6bUh5cTr7xi0jAD942wCJA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==}
|
||||
engines: {node: '>=18.0'}
|
||||
peerDependencies:
|
||||
postcss: ^8.5.25
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4295,6 +4354,20 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
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([email protected])
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -4335,6 +4408,16 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
css-what: 7.0.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
nth-check: 2.1.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -4355,10 +4438,28 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
entities: 4.5.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
[email protected]:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@drizzle-team/brocli': 0.10.2
|
||||
@@ -4381,6 +4482,10 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -4487,6 +4592,13 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
entities: 7.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -4701,6 +4813,8 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -4829,6 +4943,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -4944,6 +5062,12 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
postcss: 8.5.26
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ const pixelify = Pixelify_Sans({
|
||||
// enforceSiteAccess, DB-backed site settings) to render the <html> element,
|
||||
// which cannot be wrapped in <Suspense>. 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<Metadata> {
|
||||
const appUrl = process.env.APP_URL ?? "http://localhost:3000";
|
||||
|
||||
@@ -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<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, { furnitype: Array<Record<string, unknown>> }>;
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
|
||||
@@ -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<string> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
async function nitroPathFor(lib: string): Promise<string> {
|
||||
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<EffectListItem[]> {
|
||||
...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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<string> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
async function nitroPathFor(lib: string): Promise<string> {
|
||||
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 };
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<string, string>): 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,
|
||||
});
|
||||
|
||||
@@ -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<string> {
|
||||
|
||||
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",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { getRuntimePath } from "@/lib/utils/runtime-path";
|
||||
/**
|
||||
* Localized furnidata builder — generates FurnitureData_<lang>.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<Map<string, string>> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<string>();
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<string> {
|
||||
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<string[]> {
|
||||
@@ -48,14 +53,14 @@ export async function getFurnitureDataWritePaths(): Promise<string[]> {
|
||||
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",
|
||||
)
|
||||
|
||||
@@ -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<void> {
|
||||
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<number | undefined> {
|
||||
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 {
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" }] }),
|
||||
|
||||
@@ -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<string, Buffer | null>();
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
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`,
|
||||
);
|
||||
|
||||
@@ -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<string[]> {
|
||||
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_<code>_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`,
|
||||
|
||||
@@ -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<string[]> {
|
||||
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 */
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<string>();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Vendored
+12
-5
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user