diff --git a/Dockerfile b/Dockerfile index a99092f3..5db8b2f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ ENV APP_DATA_DIRECTORY=/app_data \ UV_LINK_MODE=copy \ PATH="/root/.local/bin:${PATH}" \ EXPORT_PACKAGE_ROOT=/app/presentation-export \ + EXPORT_RUNTIME_DIR=/app/presentation-export \ BUILT_PYTHON_MODULE_PATH=/app/presentation-export/py/convert-linux-x64 \ PRESENTON_APP_ROOT=/app @@ -26,17 +27,16 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs \ && rm -rf /var/lib/apt/lists/* +COPY package.json package-lock.json /app/ +RUN npm --prefix /app install --omit=dev + RUN mkdir -p /app/document-extraction-liteparse \ && npm --prefix /app/document-extraction-liteparse init -y \ && npm --prefix /app/document-extraction-liteparse install @llamaindex/liteparse@1.4.0 --omit=dev COPY electron/resources/document-extraction/liteparse_runner.mjs /app/document-extraction-liteparse/liteparse_runner.mjs -# PDF/PPTX export runtime: version pin in presentation-export/export-version.json (or build-arg). -COPY presentation-export/export-version.json /app/presentation-export/export-version.json COPY scripts/sync-presentation-export.cjs /app/scripts/sync-presentation-export.cjs -ARG EXPORT_RUNTIME_VERSION -RUN export EXPORT_RUNTIME_VERSION="${EXPORT_RUNTIME_VERSION:-}" \ - && node /app/scripts/sync-presentation-export.cjs --force \ +RUN node /app/scripts/sync-presentation-export.cjs --force \ && chmod +x /app/presentation-export/py/convert-linux-x64 RUN curl -fsSL https://ollama.com/install.sh | sh diff --git a/Dockerfile.dev b/Dockerfile.dev index 74503219..b1cdaf8b 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -12,6 +12,7 @@ ENV APP_DATA_DIRECTORY=/app_data \ UV_LINK_MODE=copy \ PATH="/root/.local/bin:${PATH}" \ EXPORT_PACKAGE_ROOT=/app/presentation-export \ + EXPORT_RUNTIME_DIR=/app/presentation-export \ BUILT_PYTHON_MODULE_PATH=/app/presentation-export/py/convert-linux-x64 \ PRESENTON_APP_ROOT=/app @@ -26,16 +27,16 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs \ && rm -rf /var/lib/apt/lists/* +COPY package.json package-lock.json /app/ +RUN npm --prefix /app install --omit=dev + RUN mkdir -p /app/document-extraction-liteparse \ && npm --prefix /app/document-extraction-liteparse init -y \ && npm --prefix /app/document-extraction-liteparse install @llamaindex/liteparse@1.4.0 --omit=dev COPY electron/resources/document-extraction/liteparse_runner.mjs /app/document-extraction-liteparse/liteparse_runner.mjs -COPY presentation-export/export-version.json /app/presentation-export/export-version.json COPY scripts/sync-presentation-export.cjs /app/scripts/sync-presentation-export.cjs -ARG EXPORT_RUNTIME_VERSION -RUN export EXPORT_RUNTIME_VERSION="${EXPORT_RUNTIME_VERSION:-}" \ - && node /app/scripts/sync-presentation-export.cjs --force \ +RUN node /app/scripts/sync-presentation-export.cjs --force \ && chmod +x /app/presentation-export/py/convert-linux-x64 # Bind mount `.:/app` hides any .venv under servers/fastapi at runtime — install deps into diff --git a/docker-compose.yml b/docker-compose.yml index 11b08bc6..eb25539d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,6 @@ services: build: context: . dockerfile: Dockerfile - args: - # Optional: override presentation-export release (else presentation-export/export-version.json) - EXPORT_RUNTIME_VERSION: ${EXPORT_RUNTIME_VERSION:-} ports: # You can replace 5000 with any other port number of your choice to run Presenton on a different port number. - "5000:80" @@ -45,8 +42,6 @@ services: build: context: . dockerfile: Dockerfile - args: - EXPORT_RUNTIME_VERSION: ${EXPORT_RUNTIME_VERSION:-} deploy: resources: reservations: @@ -91,14 +86,13 @@ services: build: context: . dockerfile: Dockerfile.dev - args: - EXPORT_RUNTIME_VERSION: ${EXPORT_RUNTIME_VERSION:-} ports: - "5000:80" # Required for Codex OAuth callback (OpenAI redirects browser directly to localhost:1455) - "1455:1455" volumes: - .:/app + - presenton_root_node_modules:/app/node_modules - ./app_data:/app_data environment: # Dockerfile.dev does not install ollama; use a host daemon via OLLAMA_URL or omit. @@ -132,8 +126,6 @@ services: build: context: . dockerfile: Dockerfile.dev - args: - EXPORT_RUNTIME_VERSION: ${EXPORT_RUNTIME_VERSION:-} deploy: resources: reservations: @@ -147,6 +139,7 @@ services: - "1455:1455" volumes: - .:/app + - presenton_root_node_modules:/app/node_modules - ./app_data:/app_data environment: - START_EMBEDDED_OLLAMA=false @@ -174,3 +167,6 @@ services: - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING} - COMFYUI_URL=${COMFYUI_URL} - COMFYUI_WORKFLOW=${COMFYUI_WORKFLOW} + +volumes: + presenton_root_node_modules: diff --git a/electron/servers/fastapi/services/export_task_service.py b/electron/servers/fastapi/services/export_task_service.py index 571dee01..dccba7ab 100644 --- a/electron/servers/fastapi/services/export_task_service.py +++ b/electron/servers/fastapi/services/export_task_service.py @@ -4,7 +4,6 @@ import os import shutil import subprocess import tempfile -import uuid from typing import Mapping from fastapi import HTTPException @@ -33,7 +32,7 @@ class ExportTaskService: self.timeout_seconds = timeout_seconds self.node_binary = os.getenv("LITEPARSE_NODE_BINARY", "node") self.export_dir = self._resolve_export_dir() - self.entrypoint_path = os.path.join(self.export_dir, "index.js") + self.entrypoint_path = self._resolve_entrypoint_path(self.export_dir) self.converter_path = self._resolve_converter_path(self.export_dir) @staticmethod @@ -42,31 +41,40 @@ class ExportTaskService: if configured: return configured + package_root = (os.getenv("EXPORT_PACKAGE_ROOT") or "").strip() + if package_root: + return package_root + cwd = os.path.abspath(".") service_dir = os.path.dirname(__file__) candidates = [ - os.path.abspath(os.path.join(cwd, "..", "..", "resources", "export")), - os.path.abspath(os.path.join(cwd, "..", "export")), - os.path.abspath( - os.path.join(service_dir, "..", "..", "..", "resources", "export") - ), - os.path.abspath(os.path.join(service_dir, "..", "..", "export")), - os.path.abspath( - os.path.join(cwd, "..", "..", "electron", "resources", "export") - ), - os.path.abspath( - os.path.join( - service_dir, "..", "..", "..", "..", "electron", "resources", "export" - ) - ), + os.path.abspath(os.path.join(cwd, "..", "..", "presentation-export")), + os.path.abspath(os.path.join(cwd, "..", "presentation-export")), + os.path.abspath(os.path.join(service_dir, "..", "..", "..", "presentation-export")), + os.path.abspath(os.path.join(service_dir, "..", "..", "..", "..", "presentation-export")), ] for candidate in candidates: - if os.path.isfile(os.path.join(candidate, "index.js")): + if os.path.isfile(os.path.join(candidate, "index.cjs")) or os.path.isfile( + os.path.join(candidate, "index.js") + ): return candidate return candidates[0] + @staticmethod + def _resolve_entrypoint_path(export_dir: str) -> str: + index_cjs = os.path.join(export_dir, "index.cjs") + if os.path.isfile(index_cjs): + return index_cjs + + index_js = os.path.join(export_dir, "index.js") + if os.path.isfile(index_js): + shutil.copyfile(index_js, index_cjs) + return index_cjs + + return index_cjs + @staticmethod def _resolve_converter_path(export_dir: str) -> str: py_dir = os.path.join(export_dir, "py") diff --git a/package-lock.json b/package-lock.json index e6ea511c..63b93ea8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,48 +8,555 @@ "name": "presenton", "version": "1.0.0", "dependencies": { - "react-colorful": "^5.6.1" + "sharp": "^0.34.5" } }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-colorful": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", - "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "peer": true, + "optional": true, "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" + "tslib": "^2.4.0" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", - "peer": true + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true } } } diff --git a/package.json b/package.json index 69902d5f..4571d592 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { "name": "presenton", "version": "1.0.0", + "presentationExportVersion": "v0.2.0", "type": "module", "description": "Open-source AI presentation generator", "scripts": { "sync:presentation-export": "node scripts/sync-presentation-export.cjs", "sync:presentation-export:force": "node scripts/sync-presentation-export.cjs --force", "check:presentation-export": "node scripts/sync-presentation-export.cjs --check-only" + }, + "dependencies": { + "sharp": "^0.34.5" } } diff --git a/presentation-export/export-version.json b/presentation-export/export-version.json deleted file mode 100644 index 2cbe162a..00000000 --- a/presentation-export/export-version.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "exportVersion": "v0.2.0" -} diff --git a/scripts/sync-presentation-export.cjs b/scripts/sync-presentation-export.cjs index 4933e8ea..2aa101a7 100644 --- a/scripts/sync-presentation-export.cjs +++ b/scripts/sync-presentation-export.cjs @@ -4,10 +4,10 @@ * * Version resolution (first match): * 1. EXPORT_RUNTIME_VERSION env - * 2. presentation-export/export-version.json → exportVersion + * 2. package.json → presentationExportVersion * * CLI: --force re-download even if valid runtime already exists - * --check-only verify index.js + converter exist and exit 0/1 + * --check-only verify index.cjs + converter exist and exit 0/1 */ const fs = require("fs"); const path = require("path"); @@ -18,8 +18,9 @@ const { execFileSync } = require("child_process"); const repoRoot = path.join(__dirname, ".."); const targetRoot = path.join(repoRoot, "presentation-export"); const targetPyDir = path.join(targetRoot, "py"); -const targetIndex = path.join(targetRoot, "index.js"); -const versionFile = path.join(targetRoot, "export-version.json"); +const targetIndexJs = path.join(targetRoot, "index.js"); +const targetIndexCjs = path.join(targetRoot, "index.cjs"); +const packageJsonFile = path.join(repoRoot, "package.json"); const cacheDir = path.join(repoRoot, ".cache", "presentation-export"); const exportRepoBase = "https://github.com/presenton/presenton-export/releases/download"; @@ -34,15 +35,17 @@ function ensureDir(dirPath) { } function readPinnedVersion() { - if (!fs.existsSync(versionFile)) { + if (!fs.existsSync(packageJsonFile)) { throw new Error( - `Missing ${path.relative(repoRoot, versionFile)}. Create it with { "exportVersion": "vX.Y.Z" }.` + `Missing ${path.relative(repoRoot, packageJsonFile)}. Add \"presentationExportVersion\": \"vX.Y.Z\".` ); } - const raw = JSON.parse(fs.readFileSync(versionFile, "utf8")); - const v = (raw.exportVersion || "").trim(); + const raw = JSON.parse(fs.readFileSync(packageJsonFile, "utf8")); + const v = (raw.presentationExportVersion || "").trim(); if (!v) { - throw new Error(`${versionFile} must set "exportVersion" (e.g. "v0.2.0").`); + throw new Error( + `${path.relative(repoRoot, packageJsonFile)} must set \"presentationExportVersion\" (e.g. \"v0.2.0\").` + ); } return v; } @@ -124,10 +127,32 @@ function getConverterCandidates() { ]; } -function validateExistingRuntime() { - if (!fs.existsSync(targetIndex)) { - return { ok: false, reason: `Missing runtime bundle: ${targetIndex}` }; +function ensureCommonJsEntrypoint() { + if (!fs.existsSync(targetIndexJs)) { + return { ok: false, reason: `Missing runtime bundle: ${targetIndexJs}` }; } + + if (fs.existsSync(targetIndexCjs)) { + return { ok: true, entrypointPath: targetIndexCjs }; + } + + try { + fs.copyFileSync(targetIndexJs, targetIndexCjs); + return { ok: true, entrypointPath: targetIndexCjs }; + } catch (err) { + return { + ok: false, + reason: `Failed to create CommonJS entrypoint ${targetIndexCjs}: ${err.message}`, + }; + } +} + +function validateExistingRuntime() { + const entrypoint = ensureCommonJsEntrypoint(); + if (!entrypoint.ok) { + return { ok: false, reason: entrypoint.reason }; + } + const candidates = getConverterCandidates(); const converterPath = candidates.find((c) => fs.existsSync(c)); if (!converterPath) { @@ -137,7 +162,7 @@ function validateExistingRuntime() { }; } chmodIfPossible(converterPath); - return { ok: true, converterPath }; + return { ok: true, entrypointPath: entrypoint.entrypointPath, converterPath }; } function downloadFile(url, outputPath, redirects = 5) { @@ -207,10 +232,6 @@ async function downloadAndInstallRuntime() { const tag = await getTargetVersion(); const downloadUrl = `${exportRepoBase}/${tag}/${linuxAssetName}`; - const versionPinBackup = fs.existsSync(versionFile) - ? fs.readFileSync(versionFile, "utf8") - : JSON.stringify({ exportVersion: tag }, null, 2) + "\n"; - ensureDir(cacheDir); const zipPath = path.join(cacheDir, linuxAssetName); const extractDir = path.join(cacheDir, `extract-${Date.now()}`); @@ -226,9 +247,6 @@ async function downloadAndInstallRuntime() { ensureDir(targetRoot); fs.cpSync(sourceRoot, targetRoot, { recursive: true, force: true }); - ensureDir(path.dirname(versionFile)); - fs.writeFileSync(versionFile, versionPinBackup, "utf8"); - fs.rmSync(extractDir, { recursive: true, force: true }); return { tag, downloadUrl }; @@ -242,14 +260,14 @@ async function main() { throw new Error(existing.reason); } console.log("[presentation-export] OK"); - console.log(` - ${targetIndex}`); + console.log(` - ${existing.entrypointPath}`); console.log(` - ${existing.converterPath}`); return; } if (existing.ok && !forceDownload) { console.log("[presentation-export] Using existing runtime:"); - console.log(` - ${targetIndex}`); + console.log(` - ${existing.entrypointPath}`); console.log(` - ${existing.converterPath}`); return; } @@ -263,7 +281,7 @@ async function main() { console.log("[presentation-export] Synced successfully:"); console.log(` - release: ${tag}`); console.log(` - url: ${downloadUrl}`); - console.log(` - ${targetIndex}`); + console.log(` - ${installed.entrypointPath}`); console.log(` - ${installed.converterPath}`); } diff --git a/servers/fastapi/services/export_task_service.py b/servers/fastapi/services/export_task_service.py index c975a4d3..8e9c8ac2 100644 --- a/servers/fastapi/services/export_task_service.py +++ b/servers/fastapi/services/export_task_service.py @@ -4,7 +4,6 @@ import os import shutil import subprocess import tempfile -import uuid from typing import Mapping from fastapi import HTTPException @@ -29,7 +28,7 @@ class ExportTaskService: self.timeout_seconds = timeout_seconds self.node_binary = os.getenv("LITEPARSE_NODE_BINARY", "node") self.export_dir = self._resolve_export_dir() - self.entrypoint_path = os.path.join(self.export_dir, "index.js") + self.entrypoint_path = self._resolve_entrypoint_path(self.export_dir) self.converter_path = self._resolve_converter_path(self.export_dir) @staticmethod @@ -38,31 +37,40 @@ class ExportTaskService: if configured: return configured + package_root = (os.getenv("EXPORT_PACKAGE_ROOT") or "").strip() + if package_root: + return package_root + cwd = os.path.abspath(".") service_dir = os.path.dirname(__file__) candidates = [ - os.path.abspath(os.path.join(cwd, "..", "..", "resources", "export")), - os.path.abspath(os.path.join(cwd, "..", "export")), - os.path.abspath( - os.path.join(service_dir, "..", "..", "..", "resources", "export") - ), - os.path.abspath(os.path.join(service_dir, "..", "..", "export")), - os.path.abspath( - os.path.join(cwd, "..", "..", "electron", "resources", "export") - ), - os.path.abspath( - os.path.join( - service_dir, "..", "..", "..", "..", "electron", "resources", "export" - ) - ), + os.path.abspath(os.path.join(cwd, "..", "..", "presentation-export")), + os.path.abspath(os.path.join(cwd, "..", "presentation-export")), + os.path.abspath(os.path.join(service_dir, "..", "..", "..", "presentation-export")), + os.path.abspath(os.path.join(service_dir, "..", "..", "..", "..", "presentation-export")), ] for candidate in candidates: - if os.path.isfile(os.path.join(candidate, "index.js")): + if os.path.isfile(os.path.join(candidate, "index.cjs")) or os.path.isfile( + os.path.join(candidate, "index.js") + ): return candidate return candidates[0] + @staticmethod + def _resolve_entrypoint_path(export_dir: str) -> str: + index_cjs = os.path.join(export_dir, "index.cjs") + if os.path.isfile(index_cjs): + return index_cjs + + index_js = os.path.join(export_dir, "index.js") + if os.path.isfile(index_js): + shutil.copyfile(index_js, index_cjs) + return index_cjs + + return index_cjs + @staticmethod def _resolve_converter_path(export_dir: str) -> str: py_dir = os.path.join(export_dir, "py") diff --git a/servers/nextjs/app/api/export-as-pdf/route.ts b/servers/nextjs/app/api/export-as-pdf/route.ts index 92e64fee..0cfee485 100644 --- a/servers/nextjs/app/api/export-as-pdf/route.ts +++ b/servers/nextjs/app/api/export-as-pdf/route.ts @@ -1,106 +1,10 @@ -import path from "path"; -import fs from "fs"; -import puppeteer from "puppeteer"; import { NextResponse, NextRequest } from "next/server"; -import { sanitizeFilename } from "@/app/(presentation-generator)/utils/others"; import { bundledExportPackageAvailable, runBundledPdfExport, } from "@/lib/run-bundled-pdf-export"; -async function exportPdfWithInlinePuppeteer( - id: string, - title: string | undefined -): Promise<{ path: string }> { - let nextjsUrl = process.env.NEXT_PUBLIC_URL; - if (!nextjsUrl) { - nextjsUrl = "http://127.0.0.1"; - } - - const browser = await puppeteer.launch({ - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, - headless: true, - args: [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - "--disable-gpu", - "--disable-web-security", - "--disable-background-timer-throttling", - "--disable-backgrounding-occluded-windows", - "--disable-renderer-backgrounding", - "--disable-features=TranslateUI", - "--disable-ipc-flooding-protection", - ], - }); - const page = await browser.newPage(); - await page.setViewport({ width: 1280, height: 720 }); - page.setDefaultNavigationTimeout(300000); - page.setDefaultTimeout(300000); - - await page.goto(`${nextjsUrl}/pdf-maker?id=${id}`, { - waitUntil: "networkidle0", - timeout: 300000, - }); - - await page.waitForFunction('() => document.readyState === "complete"'); - - try { - await page.waitForFunction( - ` - () => { - const allElements = document.querySelectorAll('*'); - let loadedElements = 0; - let totalElements = allElements.length; - - for (let el of allElements) { - const style = window.getComputedStyle(el); - const isVisible = style.display !== 'none' && - style.visibility !== 'hidden' && - style.opacity !== '0'; - - if (isVisible && el.offsetWidth > 0 && el.offsetHeight > 0) { - loadedElements++; - } - } - - return (loadedElements / totalElements) >= 0.99; - } - `, - { timeout: 300000 } - ); - - await new Promise((resolve) => setTimeout(resolve, 1000)); - } catch (error) { - console.log("Warning: Some content may not have loaded completely:", error); - } - - const pdfBuffer = await page.pdf({ - width: "1280px", - height: "720px", - printBackground: true, - margin: { top: 0, right: 0, bottom: 0, left: 0 }, - }); - - await browser.close(); - - const sanitizedTitle = sanitizeFilename(title ?? "presentation"); - const appDataDirectory = process.env.APP_DATA_DIRECTORY!; - if (!appDataDirectory) { - throw new Error("App data directory not found"); - } - const destinationPath = path.join( - appDataDirectory, - "exports", - `${sanitizedTitle}.pdf` - ); - await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true }); - await fs.promises.writeFile(destinationPath, pdfBuffer); - - return { path: destinationPath }; -} - export async function POST(req: NextRequest) { const { id, title } = await req.json(); if (!id) { @@ -111,18 +15,16 @@ export async function POST(req: NextRequest) { } try { - if (await bundledExportPackageAvailable()) { - const { path: outPath } = await runBundledPdfExport({ - presentationId: id, - title, - }); - return NextResponse.json({ - success: true, - path: outPath, - }); + if (!(await bundledExportPackageAvailable())) { + throw new Error( + "presentation-export runtime is not available. Run scripts/sync-presentation-export.cjs to install it." + ); } - const { path: outPath } = await exportPdfWithInlinePuppeteer(id, title); + const { path: outPath } = await runBundledPdfExport({ + presentationId: id, + title, + }); return NextResponse.json({ success: true, path: outPath, diff --git a/servers/nextjs/lib/run-bundled-pdf-export.ts b/servers/nextjs/lib/run-bundled-pdf-export.ts index 06495554..c6715a25 100644 --- a/servers/nextjs/lib/run-bundled-pdf-export.ts +++ b/servers/nextjs/lib/run-bundled-pdf-export.ts @@ -19,6 +19,20 @@ export function getPresentonAppRoot(): string { ); } +async function resolveExportEntrypoint(exportRoot: string): Promise { + const indexCjs = path.join(exportRoot, "index.cjs"); + const indexJs = path.join(exportRoot, "index.js"); + + try { + await fs.access(indexCjs); + return indexCjs; + } catch { + await fs.access(indexJs); + await fs.copyFile(indexJs, indexCjs); + return indexCjs; + } +} + function bundledConverterPath(exportRoot: string): string { const fromEnv = process.env.BUILT_PYTHON_MODULE_PATH?.trim(); if (fromEnv) { @@ -35,7 +49,7 @@ function bundledConverterPath(exportRoot: string): string { export async function bundledExportPackageAvailable(): Promise { try { const root = getExportPackageRoot(); - await fs.access(path.join(root, "index.js")); + await resolveExportEntrypoint(root); await fs.access(bundledConverterPath(root)); return true; } catch { @@ -45,6 +59,53 @@ export async function bundledExportPackageAvailable(): Promise { export type BundledPdfExportResult = { path: string }; +function normalizeExportOutputPath(params: { + pathValue?: string; + urlValue?: string; +}): string { + const { pathValue, urlValue } = params; + const appData = process.env.APP_DATA_DIRECTORY?.trim(); + + const resolveAppDataRelative = (value: string): string => { + if (!appData) { + throw new Error("APP_DATA_DIRECTORY is required for relative export paths."); + } + + const normalized = value.startsWith("/") ? value.slice(1) : value; + if (!normalized.startsWith("app_data/")) { + return path.join(appData, normalized); + } + return path.join(appData, normalized.slice("app_data/".length)); + }; + + if (pathValue && typeof pathValue === "string") { + if (path.isAbsolute(pathValue)) { + return pathValue; + } + return resolveAppDataRelative(pathValue); + } + + if (urlValue && typeof urlValue === "string") { + if (urlValue.startsWith("file://")) { + const parsed = new URL(urlValue); + const fsPath = decodeURIComponent(parsed.pathname || ""); + if (fsPath.startsWith("/app_data/")) { + return resolveAppDataRelative(fsPath); + } + if (path.isAbsolute(fsPath)) { + return fsPath; + } + return resolveAppDataRelative(fsPath); + } + + if (urlValue.startsWith("/app_data/")) { + return resolveAppDataRelative(urlValue); + } + } + + throw new Error("Export finished but response did not include a valid output path."); +} + /** * Runs the bundled export entrypoint (`presentation-export/index.js`) with * `BUILT_PYTHON_MODULE_PATH` pointing at the PyInstaller converter binary. @@ -55,11 +116,10 @@ export async function runBundledPdfExport(params: { }): Promise { const { presentationId, title } = params; const exportRoot = getExportPackageRoot(); - const indexJs = path.join(exportRoot, "index.js"); + const entrypoint = await resolveExportEntrypoint(exportRoot); const converter = bundledConverterPath(exportRoot); const appRoot = getPresentonAppRoot(); - await fs.access(indexJs); await fs.access(converter); const nextjsUrl = @@ -90,7 +150,7 @@ export async function runBundledPdfExport(params: { const responsePath = exportTaskPath.replace(/\.json$/i, ".response.json"); await new Promise((resolve, reject) => { - const child = spawn(process.execPath, [indexJs, exportTaskPath], { + const child = spawn(process.execPath, [entrypoint, exportTaskPath], { cwd: appRoot, stdio: ["ignore", "pipe", "pipe"], env: { @@ -120,20 +180,12 @@ export async function runBundledPdfExport(params: { }); const responseRaw = await fs.readFile(responsePath, "utf8"); - const responseData = JSON.parse(responseRaw) as { path?: string }; + const responseData = JSON.parse(responseRaw) as { path?: string; url?: string }; - if (!responseData?.path || typeof responseData.path !== "string") { - throw new Error("Export finished but response did not include a path."); - } - - let outPath = responseData.path; - if (!path.isAbsolute(outPath)) { - const appData = process.env.APP_DATA_DIRECTORY?.trim(); - if (!appData) { - throw new Error("APP_DATA_DIRECTORY is required for relative export paths."); - } - outPath = path.join(appData, outPath); - } + const outPath = normalizeExportOutputPath({ + pathValue: responseData?.path, + urlValue: responseData?.url, + }); return { path: outPath }; } diff --git a/start.js b/start.js index f5748534..77e6a778 100644 --- a/start.js +++ b/start.js @@ -10,6 +10,7 @@ const __dirname = dirname(__filename); const fastapiDir = join(__dirname, "servers/fastapi"); const nextjsDir = join(__dirname, "servers/nextjs"); +const exportSyncScript = join(__dirname, "scripts/sync-presentation-export.cjs"); const args = process.argv.slice(2); const hasDevArg = args.includes("--dev") || args.includes("-d"); @@ -55,6 +56,52 @@ const setupNodeModules = () => { }); }; +const runNodeScript = (scriptPath, scriptArgs) => { + return new Promise((resolve, reject) => { + const scriptProcess = spawn(process.execPath, [scriptPath, ...scriptArgs], { + cwd: __dirname, + stdio: "inherit", + env: process.env, + }); + + scriptProcess.on("error", (err) => { + reject(err); + }); + + scriptProcess.on("exit", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Script failed with exit code: ${code}`)); + } + }); + }); +}; + +const ensurePresentationExportRuntime = async () => { + if (process.env.ENSURE_PRESENTATION_EXPORT_RUNTIME === "false") { + return; + } + + if (!existsSync(exportSyncScript)) { + console.warn("presentation-export sync script not found; skipping runtime check"); + return; + } + + try { + await runNodeScript(exportSyncScript, ["--check-only"]); + } catch (err) { + if (!isDev) { + throw new Error( + "presentation-export runtime is missing in this container image. Rebuild the image so the runtime package is installed." + ); + } + + console.warn("presentation-export runtime missing in dev mount. Syncing runtime package..."); + await runNodeScript(exportSyncScript, ["--force"]); + } +}; + process.env.USER_CONFIG_PATH = userConfigPath; //? UserConfig is only setup if API Keys can be changed @@ -223,6 +270,8 @@ const startNginx = () => { }; const main = async () => { + await ensurePresentationExportRuntime(); + if (isDev) { await setupNodeModules(); }