Skip to content

Fix: transform() return type to MaybePromise and handle async array case (fixes #495)#611

Open
shivamtiwari3 wants to merge 3 commits intomarkdoc:mainfrom
shivamtiwari3:fix/transform-maybe-promise-type-495
Open

Fix: transform() return type to MaybePromise and handle async array case (fixes #495)#611
shivamtiwari3 wants to merge 3 commits intomarkdoc:mainfrom
shivamtiwari3:fix/transform-maybe-promise-type-495

Conversation

@shivamtiwari3
Copy link
Copy Markdown

@shivamtiwari3 shivamtiwari3 commented Mar 15, 2026

Summary

Fixes #495.

Markdoc.transform() declared sync-only return types (RenderableTreeNode / RenderableTreeNode[]) even though Node.transform() already returns MaybePromise<RenderableTreeNodes>. This made it impossible for callers to correctly type-check or await async transforms.


Root Cause

In index.ts, the two exported overload signatures for transform() were:

export function transform(node: Node, config?: C): RenderableTreeNode;
export function transform(nodes: Node[], config?: C): RenderableTreeNode[];

But Node.transform() (in src/ast/node.ts:81) returns MaybePromise<RenderableTreeNodes>, meaning schemas that define an async transform() would produce a Promise at runtime — yet the declared return type claimed it was always synchronous. TypeScript would flag any await Markdoc.transform(...) call as "result is not a Promise".

Additionally, the array path in the implementation called content.flatMap((child) => child.transform(config)) without checking for Promise results, so async children would be silently embedded in the returned array as unresolved Promise objects rather than being correctly awaited.


Solution

  1. Update overload signatures to MaybePromise<RenderableTreeNode> / MaybePromise<RenderableTreeNode[]>, matching the return type contract already established by Node.transform().

  2. Fix the array-case implementation to mirror the Promise.all pattern already used in transformer.children() (src/transformer.ts:62-68): collect child results, check if any are Promises, and if so return Promise.all(results).then(...).


Testing

Added src/transformer.test.ts with 5 tests covering:

  • Sync path: single Node returns non-Promise Tag
  • Sync path: Node[] returns non-Promise RenderableTreeNode[]
  • Async path: single Node with async schema returns a Promise<Tag>
  • Async path: Node[] where one child uses async schema returns Promise<RenderableTreeNode[]>
  • Async path: document-order preservation across multiple async transforms

All 264 existing specs pass. tsc --noEmit clean (pre-existing React errors in renderers unaffected).

Run with:

npm test
npm run type:check

Checklist

  • Fixes the root cause (not just the symptom)
  • New tests cover the exact failing scenario from the issue
  • All existing tests pass (264 specs, 0 failures)
  • No unrelated changes
  • Code style matches project conventions
  • Read CONTRIBUTING.md and followed its requirements

…ase (fixes markdoc#495)

Root cause: Node.transform() returns MaybePromise<RenderableTreeNodes> but the
exported transform() overloads declared sync-only return types. The array path
also used flatMap directly without checking for Promise results, so async schemas
would silently return unresolved Promises instead of awaitable values.

Fix: Update overload signatures to MaybePromise<RenderableTreeNode> /
MaybePromise<RenderableTreeNode[]> and mirror the Promise.all pattern from
transformer.children() in the array case so async transforms are correctly
aggregated and exposed to callers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exported transform type should return MaybePromise

1 participant