diff --git a/.gitignore b/.gitignore index 2098bb3..4df9e01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ coverage/ +dist/ data/ .env .env.* diff --git a/Dockerfile b/Dockerfile index b2752ae..af85d59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,20 @@ -FROM node:20-alpine +# Multi-stage: esbuild produces a single Node file (deps inlined) + minified UI. +# Final image has no node_modules — only dist/ + runtime Node. + +FROM node:20-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --omit=dev +RUN npm ci COPY src/ ./src/ COPY ui/ ./ui/ +COPY scripts/ ./scripts/ +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +ENV NODE_ENV=production +# Runtime is only the bundled server + UI (no node_modules, no source, no mcp-only entry) +COPY --from=build /app/dist/server.js ./dist/server.js +COPY --from=build /app/dist/ui ./dist/ui EXPOSE 3001 -CMD ["node", "src/index.js"] +CMD ["node", "dist/server.js"] diff --git a/README.md b/README.md index e83144c..7d3060d 100644 --- a/README.md +++ b/README.md @@ -19,22 +19,34 @@ Webshare download links expire after roughly ten minutes, so the torrent never e ## Layout ``` -src/ - index.js # Express app + optional Sonarr missing-episode search - webshare.js # salt/login, search, file_link - md5crypt.js # Unix $1$ MD5-crypt (Webshare password digest) - torznab.js # caps + search feed XML - torrent.js # bencode + fake torrent + info-hash - qbt.js # qBittorrent Web API façade + download queue - mcp-server.js # MCP tool registration (shared) - mcp-http.js # Streamable HTTP transport mount for Express - mcp.js # Optional MCP-only HTTP process - ui/ # Web UI: search & queue management +src/ # source (CommonJS modules) +ui/ # Web UI source (index.html, styles.css, scripts.js) +scripts/build.mjs # esbuild: single server.js + minified single-file UI +dist/ # build output (gitignored; produced by npm run build / Docker) + server.js # one file: app + all npm dependencies + mcp.js # optional MCP-only entry + ui/index.html # CSS+JS inlined __tests__/ compose.yaml -Dockerfile +Dockerfile # multi-stage: build with esbuild, runtime = Node + dist only ``` +## Build (smaller image / single JS) + +```bash +npm install +npm run build # → dist/server.js (~2 MB, all deps bundled) + dist/ui/index.html +npm start # node dist/server.js +npm run start:dev # unbundled src/ for local hacking +``` + +**esbuild** bundles the server (and optional `mcp` entry) into a **single `.js` file** with +dependencies inlined — no `node_modules` needed at runtime. The Web UI is minified into +**one** `dist/ui/index.html` (CSS and JS inlined). + +Docker uses a multi-stage build: final image copies only `dist/`, not source or +`node_modules`, which keeps the image much smaller than `npm ci --omit=dev`. + ## Configuration Environment variables (required unless noted): @@ -71,9 +83,11 @@ See `compose.yaml`. Override ports, `BASE_URL`, Sonarr settings, and volume moun ```bash npm install +npm run build export WEBSHARE_USERNAME=... export WEBSHARE_PASSWORD=... npm start +# or: npm run start:dev # no build step, uses src/ ``` ## Sonarr / Radarr diff --git a/package-lock.json b/package-lock.json index ab457b1..56009fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,11 @@ "xml2js": "^0.6.0", "zod": "^4.4.3" }, + "bin": { + "webshare-mcp": "dist/mcp.js" + }, "devDependencies": { + "esbuild": "^0.25.0", "jest": "^30.4.2", "supertest": "^7.2.2" }, @@ -603,6 +607,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@hono/node-server": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.0.tgz", @@ -2946,6 +3392,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", diff --git a/package.json b/package.json index f0c26bf..e93183d 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,19 @@ { "name": "webshare-api", "version": "1.0.0", - "description": "Webshare.cz HTTP API: Torznab indexer, fake qBittorrent client, and MCP server for AI agents", - "main": "src/index.js", + "description": "Webshare.cz HTTP API: Torznab indexer, fake qBittorrent client, Web UI, and MCP server for AI agents", + "main": "dist/server.js", "bin": { - "webshare-mcp": "src/mcp.js" + "webshare-mcp": "dist/mcp.js" }, "scripts": { - "start": "node src/index.js", - "mcp": "node src/mcp.js", - "test": "jest --coverage --runInBand" + "build": "node scripts/build.mjs", + "start": "node dist/server.js", + "start:dev": "node src/index.js", + "mcp": "node dist/mcp.js", + "mcp:dev": "node src/mcp.js", + "test": "jest --coverage --runInBand", + "prepublishOnly": "npm run build" }, "engines": { "node": ">=18" @@ -23,7 +27,15 @@ "zod": "^4.4.3" }, "devDependencies": { + "esbuild": "^0.25.0", "jest": "^30.4.2", "supertest": "^7.2.2" - } + }, + "files": [ + "dist", + "src", + "ui", + "LICENSE", + "README.md" + ] } diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..c06f645 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/** + * Production build: one Node bundle (all deps inlined) + minified single-file Web UI. + * + * Outputs: + * dist/server.js — main app (Torznab, qBt, MCP, UI API) + * dist/mcp.js — optional MCP-only process + * dist/ui/index.html — Search/Queue UI (CSS+JS inlined) + */ +import * as esbuild from 'esbuild'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const dist = path.join(root, 'dist'); +const distUi = path.join(dist, 'ui'); + +fs.rmSync(dist, { recursive: true, force: true }); +fs.mkdirSync(distUi, { recursive: true }); + +const nodeBundle = { + bundle: true, + platform: 'node', + target: 'node20', + format: 'cjs', + minify: true, + legalComments: 'none', + sourcemap: false, + // Keep Node builtins external; pull npm deps into the file. + packages: 'bundle', +}; + +async function bundleServer() { + const result = await esbuild.build({ + ...nodeBundle, + entryPoints: [path.join(root, 'src/index.js')], + outfile: path.join(dist, 'server.js'), + metafile: true, + logLevel: 'info', + }); + const out = result.metafile.outputs[Object.keys(result.metafile.outputs)[0]]; + console.log(`server.js ${(out.bytes / 1024 / 1024).toFixed(2)} MB`); +} + +async function bundleMcp() { + const result = await esbuild.build({ + ...nodeBundle, + entryPoints: [path.join(root, 'src/mcp.js')], + outfile: path.join(dist, 'mcp.js'), + banner: { js: '#!/usr/bin/env node\n' }, + metafile: true, + logLevel: 'info', + }); + const out = result.metafile.outputs[Object.keys(result.metafile.outputs)[0]]; + console.log(`mcp.js ${(out.bytes / 1024 / 1024).toFixed(2)} MB`); + try { + fs.chmodSync(path.join(dist, 'mcp.js'), 0o755); + } catch { + /* windows */ + } +} + +async function bundleUi() { + const cssSrc = fs.readFileSync(path.join(root, 'ui/styles.css'), 'utf8'); + const jsSrc = fs.readFileSync(path.join(root, 'ui/scripts.js'), 'utf8'); + const htmlSrc = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8'); + + const css = await esbuild.transform(cssSrc, { loader: 'css', minify: true }); + const js = await esbuild.transform(jsSrc, { + loader: 'js', + minify: true, + target: ['es2020'], + }); + + // Prefer a single HTML payload (no extra static requests) for the tiny UI. + let html = htmlSrc + .replace(/\s*/i, '') + .replace(/\n`, + ); + + const outHtml = path.join(distUi, 'index.html'); + fs.writeFileSync(outHtml, html); + console.log(`ui/index.html ${(Buffer.byteLength(html) / 1024).toFixed(1)} KB (inlined css+js)`); +} + +await bundleServer(); +await bundleMcp(); +await bundleUi(); +console.log('build ok → dist/'); diff --git a/src/index.js b/src/index.js index b22ad60..7df74d1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +const fs = require('fs'); +const path = require('path'); const express = require('express'); const { WebshareClient } = require('./webshare'); const { caps, feed } = require('./torznab'); @@ -113,8 +115,13 @@ app.get('/resolve/:ident', async (req, res) => { } }); -// ── UI static files ──────────────────────────────────────────────────────── -app.use(express.static('ui')); +// ── UI static files (dev: ../ui from src/; production: dist/ui next to server.js) +const uiRoot = + process.env.UI_DIR || + (fs.existsSync(path.join(__dirname, 'ui')) + ? path.join(__dirname, 'ui') + : path.join(__dirname, '..', 'ui')); +app.use(express.static(uiRoot)); // ── UI API: JSON search ─────────────────────────────────────────────────── app.get('/api/search', async (req, res) => {