2026-07-13 11:42:26 +02:00

3.0 KiB

Library Architecture and Workflow

library/: builds @penpot/library; JS-facing in-memory Penpot file builder and .penpot ZIP exporter. Separate from main app runtime.

Layout and commands

  • Source: library/src/; tests: library/test/; experimentation/docs: playground/, docs/; config: shadow-cljs.edn, deps.edn, package.json.
  • From library/: build pnpm run build; bundle helper pnpm run build:bundle or ./scripts/build; tests pnpm run test; watch pnpm run watch / pnpm run watch:test; lint pnpm run lint; format check/fix pnpm run check-fmt / pnpm run fmt.
  • When changing file-format construction or export behavior in common/, consider whether @penpot/library should be tested because it constructs Penpot files outside the app UI.
  • Cross-cutting testing principles and anti-patterns: mem:testing.

JS API and builder state

  • The JS build context wraps an atom and implements IDeref; getInternalState exposes the CLJ state converted to JS.
  • Public methods decode JS objects through the JSON transformer before calling common builder functions. Exceptions become JS BuilderError objects with enumerable cause and an explain getter for Malli explain data.
  • create-build-context can store an optional referer, later written into the export manifest.
  • The builder is stateful: call addFile before addPage. addPage resets the frame/group stack to the root and clears page-local naming state when the page closes.
  • addBoard and addGroup push onto the parent stack; matching close calls pop it. closeGroup requires at least one child and recalculates group geometry. Masked groups use the first child as mask and copy its geometry.
  • commit-shape emits :add-obj with :ignore-touched true, using the current parent, frame, and page from the stack.
  • Layer names are uniqued per current page; duplicate names get generated suffixes.
  • addBool converts an existing group into a bool shape and updates style/content/geometry via :mod-obj operations rather than adding a new object.
  • Media blobs are stored separately from file-media metadata; add-file-media requires a BlobWrapper.

Export package

  • .penpot ZIPs include manifest.json, file/page/shape JSON, components/colors/typographies/tokens, media metadata, and media object blobs.
  • Path/bool shape :content is converted to vectors before JSON encoding.
  • File export intentionally includes only selected top-level attrs plus data options; color export removes :file-id and drops empty paths.
  • Manifest type is penpot/export-files, version 1, generated by penpot-library/%version%, with optional referer and file relations.
  • Export generation is sequential and lazy: delayed JSON/blob work is computed only as each zip entry is written, and the progress callback receives {total,item,path} after each entry.
  • The library has compatibility defaults for features/migrations in the common builder; do not assume it always exports with the newest app-default migrations/features.