Files
suaveplan/openspec/changes/enterprise-server-pdf/proposal.md
2026-05-27 23:29:53 +01:00

3.8 KiB

OpenSpec Change: enterprise-server-pdf

  • Change ID: enterprise-server-pdf
  • Package: @suaveplan/pdf
  • Category: server/services
  • Target path: server/services/pdf
  • Status: PROPOSED
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/pdf.md

Summary

Deliver @suaveplan/pdf on the genesis branch from scratch at enterprise quality. The package provides server-side PDF generation for receipts, reports, and invoices. It exposes a PdfBuilder with a fluent page/section/element composition API and a pluggable PdfRenderer interface, shipping two concrete renderers: a PuppeteerRenderer (HTML → PDF via headless Chrome) and a PdfKitRenderer (pure JS, no browser dependency). Zod-validated options, @suaveplan/error codes, OTel spans per render, @suaveplan/templates for HTML template rendering, and AbortSignal support for long renders.


Current-state audit (genesis)

Does not exist on genesis — fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) Typed PDF.* error codes
@suaveplan/types (workspace:^) LoggerLike
@suaveplan/templates (workspace:^) HTML template rendering
@suaveplan/logger (workspace:^) Structured logs
zod (catalog:forms) Options validation
@opentelemetry/api (catalog:observability) Spans + counters + histograms
puppeteer-core (catalog:services) Headless Chrome (optional renderer)
pdfkit (catalog:services) Pure-JS PDF generation (optional renderer)
tslib (catalog:build) Runtime helpers

Public API surface

interface PdfRenderer {
  render(document: PdfDocument, options?: RenderOptions): Promise<Uint8Array>
  dispose(): Promise<void>
}

class PdfBuilder {
  page(options?: PageOptions): this
  text(content: string, options?: TextOptions): this
  heading(content: string, level?: 1 | 2 | 3): this
  table(data: TableData, options?: TableOptions): this
  image(src: string | Uint8Array, options?: ImageOptions): this
  spacer(height?: number): this
  footer(content: string): this
  header(content: string): this
  build(): PdfDocument
}

function createPdfBuilder(): PdfBuilder
function createPuppeteerRenderer(options?: PuppeteerRendererOptions): PdfRenderer
function createPdfKitRenderer(options?: PdfKitRendererOptions): PdfRenderer

interface PdfDocument {
  pages: PdfPage[]
  metadata: PdfMetadata
}

interface PdfMetadata {
  title?: string
  author?: string
  subject?: string
  keywords?: string[]
  creator?: string
}

interface RenderOptions {
  signal?: AbortSignal
  format?: 'A4' | 'Letter' | 'Legal'
  landscape?: boolean
  margins?: { top: number; right: number; bottom: number; left: number }
}

type PdfErrorCode =
  | 'PDF_RENDER_FAILED'
  | 'PDF_TIMEOUT'
  | 'PDF_INVALID_CONTENT'
  | 'PDF_RENDERER_UNAVAILABLE'

function registerPdfErrorCodes(): void

File layout

server/services/pdf/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/index.ts
    types/
      pdf.types.ts
      pdf.types.md
    builder/
      pdf-builder/
        pdf-builder.ts
        pdf-builder.md
        pdf-builder.types.ts
        pdf-builder.test.ts
        index.ts
    renderers/
      puppeteer-renderer/
        puppeteer-renderer.ts
        puppeteer-renderer.md
        puppeteer-renderer.types.ts
        puppeteer-renderer.test.ts
        index.ts
      pdfkit-renderer/
        pdfkit-renderer.ts
        pdfkit-renderer.md
        pdfkit-renderer.types.ts
        pdfkit-renderer.test.ts
        index.ts
  tests/
    integration.test.ts

Impact

Downstream: invoice generation, report exports, document archiving. Server-only.

Prerequisites on Verdaccio: @suaveplan/error, @suaveplan/types, @suaveplan/templates, @suaveplan/logger.