Refactor presentation export runtime handling

- Update sync-presentation-export script to read version from package.json instead of export-version.json.
- Change entrypoint from index.js to index.cjs for CommonJS compatibility.
- Implement logic to ensure CommonJS entrypoint is created if missing.
- Modify ExportTaskService to resolve entrypoint path dynamically.
- Remove inline Puppeteer PDF export logic in favor of bundled export package.
- Add runtime availability checks and sync script execution in start.js.
- Introduce helper functions for output path normalization and entrypoint resolution.
This commit is contained in:
sudipnext 2026-04-18 16:49:42 +05:45
parent 8c5fd218d1
commit 5e4ee9e039
12 changed files with 778 additions and 236 deletions

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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")

577
package-lock.json generated
View file

@ -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
}
}
}

View file

@ -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"
}
}

View file

@ -1,3 +0,0 @@
{
"exportVersion": "v0.2.0"
}

View file

@ -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}`);
}

View file

@ -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")

View file

@ -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,

View file

@ -19,6 +19,20 @@ export function getPresentonAppRoot(): string {
);
}
async function resolveExportEntrypoint(exportRoot: string): Promise<string> {
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<boolean> {
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<boolean> {
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<BundledPdfExportResult> {
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<void>((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 };
}

View file

@ -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();
}