diff --git a/.changeset/feat-libreoffice-extension.md b/.changeset/feat-libreoffice-extension.md new file mode 100644 index 00000000000..ac1da2ddf96 --- /dev/null +++ b/.changeset/feat-libreoffice-extension.md @@ -0,0 +1,8 @@ +--- +"@trigger.dev/build": minor +"references/libreoffice-convert": patch +--- + +feat(build): add `libreoffice()` build extension for docx/pptx to PDF conversion + +Adds a `libreoffice(options?)` BuildExtension that installs LibreOffice system packages and `libreoffice-convert` npm package at deploy time, enabling direct Office-to-PDF conversion in Trigger.dev tasks. diff --git a/packages/build/src/extensions/core.ts b/packages/build/src/extensions/core.ts index 62671dfd571..971cf83ca68 100644 --- a/packages/build/src/extensions/core.ts +++ b/packages/build/src/extensions/core.ts @@ -5,3 +5,5 @@ export * from "./core/aptGet.js"; export * from "./core/ffmpeg.js"; export * from "./core/neonSyncEnvVars.js"; export * from "./core/vercelSyncEnvVars.js"; +export * from "./core/syncSupabaseEnvVars.js"; +export * from "./core/libreoffice.js"; diff --git a/packages/build/src/extensions/core/libreoffice.ts b/packages/build/src/extensions/core/libreoffice.ts new file mode 100644 index 00000000000..87eaa2458ae --- /dev/null +++ b/packages/build/src/extensions/core/libreoffice.ts @@ -0,0 +1,109 @@ +import { BuildExtension } from "@trigger.dev/core/v3/build"; +import { dirname } from "node:path"; +import { readPackageJSON } from "pkg-types"; + +export type LibreOfficeOptions = { + /** + * Use the minimal LibreOffice writer package (libreoffice-writer) instead of full LibreOffice. + * Sufficient for docx/pptx/xlsx to PDF conversion. Saves ~400MB vs. full install. + * @default false + */ + minimal?: boolean; +}; + +/** + * Build extension that installs LibreOffice for docx/pptx/xlsx → PDF conversion. + * + * Adds the `libreoffice-convert` npm package and the required system packages + * to the Docker build image. The extension is scoped to "deploy" target only + * (dev uses the host machine's LibreOffice if available). + * + * @example + * ```ts + * // In trigger.config.ts + * import { libreoffice } from "@trigger.dev/build/extensions/core"; + * + * build: { + * extensions: [ + * libreoffice(), + * ] + * } + * ``` + * + * Then in your task: + * ```ts + * import libreofficeConvert from "libreoffice-convert"; + * // Convert docx → PDF + * const pdfBuffer = await libreofficeConvert(docxBuffer, "pdf"); + * ``` + */ +export function libreoffice(options: LibreOfficeOptions = {}): BuildExtension { + const { minimal = false } = options; + + // The npm package used for conversion + const NPM_PACKAGE = "libreoffice-convert"; + + return { + name: "libreoffice", + + async onBuildStart(context) { + if (context.target !== "deploy") { + return; + } + + // ── System packages (apt) ───────────────────────────────────────────── + // `aptGet` is defined in the same core directory. + // We delegate to it by registering an apt-get layer. + const systemPackages = minimal + ? [ + "libreoffice-writer", + "libreoffice-calc", + "libreoffice-impress", + "fonts-liberation", + "--no-install-recommends", + ] + : [ + "libreoffice", + "--no-install-recommends", + ]; + + context.addLayer({ + id: "libreoffice-apt", + image: { + pkgs: systemPackages, + }, + }); + + // ── npm package ──────────────────────────────────────────────────────── + // Resolve the locally installed version of libreoffice-convert if present, + // otherwise fall back to "latest". + let version = "latest"; + try { + const modulePath = await context.resolvePath(NPM_PACKAGE); + if (modulePath) { + const packageJSON = await readPackageJSON(dirname(modulePath)); + version = packageJSON.version ?? "latest"; + context.logger.debug( + `[libreoffice] Resolved ${NPM_PACKAGE} version: ${version}` + ); + } + } catch (error) { + context.logger.debug( + `[libreoffice] Could not resolve ${NPM_PACKAGE} version, using "latest"`, + { error } + ); + } + + context.addLayer({ + id: "libreoffice-npm", + dependencies: { + [NPM_PACKAGE]: version, + }, + }); + + context.logger.debug( + `[libreoffice] Added LibreOffice layer (minimal=${minimal})` + ); + }, + }; +} \ No newline at end of file diff --git a/references/libreoffice-convert/.gitignore b/references/libreoffice-convert/.gitignore new file mode 100644 index 00000000000..c2658d7d1b3 --- /dev/null +++ b/references/libreoffice-convert/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/references/libreoffice-convert/package.json b/references/libreoffice-convert/package.json new file mode 100644 index 00000000000..e6feac1bfe8 --- /dev/null +++ b/references/libreoffice-convert/package.json @@ -0,0 +1,18 @@ +{ + "name": "references-libreoffice-convert", + "private": true, + "type": "module", + "devDependencies": { + "trigger.dev": "workspace:*" + }, + "dependencies": { + "@trigger.dev/build": "workspace:*", + "@trigger.dev/sdk": "workspace:*", + "libreoffice-convert": "^1.4.3", + "zod": "3.25.76" + }, + "scripts": { + "dev": "trigger dev", + "deploy": "trigger deploy" + } +} \ No newline at end of file diff --git a/references/libreoffice-convert/src/index.ts b/references/libreoffice-convert/src/index.ts new file mode 100644 index 00000000000..30707490e72 --- /dev/null +++ b/references/libreoffice-convert/src/index.ts @@ -0,0 +1,41 @@ +import { createTask } from "@trigger.dev/sdk/v3"; +import libreofficeConvert from "libreoffice-convert"; +import { z } from "zod"; + +/** + * Task that converts Office documents (docx, pptx, xlsx) to PDF using LibreOffice. + * Requires the `libreoffice()` build extension in trigger.config.ts. + * + * @example + * ```ts + * await client.tasks.call("libreoffice-convert", { + * params: { + * input: docxBuffer, // Buffer containing the Office document + * outputFormat: "pdf", // Output format (default: "pdf") + * } + * }); + * ``` + */ +export const libreofficeConvertTask = createTask({ + rpc: "libreoffice/convert", + queue: { + name: "libreoffice-convert", + parallelLimit: 2, + }, + schema: z.object({ + /** Buffer containing the Office document (docx, pptx, xlsx) */ + input: z.string().describe("Base64-encoded document buffer"), + /** Output format. Only "pdf" is supported by libreoffice-convert. */ + outputFormat: z.string().optional().default("pdf"), + }), + async run(params): Promise<{ output: string }> { + const inputBuffer = Buffer.from(params.input, "base64"); + const format = (params.outputFormat || "pdf") as Parameters[1]; + + const pdfBuffer = await libreofficeConvert(inputBuffer, format); + + return { + output: pdfBuffer.toString("base64"), + }; + }, +}); \ No newline at end of file diff --git a/references/libreoffice-convert/trigger.config.ts b/references/libreoffice-convert/trigger.config.ts new file mode 100644 index 00000000000..8cb1ae9bc6d --- /dev/null +++ b/references/libreoffice-convert/trigger.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "@trigger.dev/sdk/v3"; +import { syncEnvVars } from "@trigger.dev/build/extensions/core"; +import { libreoffice } from "@trigger.dev/build/extensions/core"; + +export default defineConfig({ + compatibilityFlags: ["run_engine_v2"], + project: "proj_rrkpdguyagvsoktglnod", + logLevel: "debug", + build: { + extensions: [ + syncEnvVars(), + libreoffice(), + ], + }, +}); \ No newline at end of file diff --git a/references/libreoffice-convert/tsconfig.json b/references/libreoffice-convert/tsconfig.json new file mode 100644 index 00000000000..9a5ee0b9d68 --- /dev/null +++ b/references/libreoffice-convert/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "Node16", + "moduleResolution": "Node16", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "customConditions": ["@triggerdotdev/source"], + "jsx": "preserve", + "lib": ["DOM", "DOM.Iterable"], + "noEmit": true + }, + "include": ["./src/**/*.ts", "trigger.config.ts"] +}