Skip to content

ESM Migration Project Plan for @patternfly/documentation-framework [draft] #4957

@nicolethoen

Description

@nicolethoen

ESM Migration Project Plan for @patternfly/documentation-framework

The documentation-framework's build pipeline depends on unified v9, remark-parse v8, and
several remark plugins at versions from 2020-2021. These packages are all CommonJS. The
broader JavaScript ecosystem — including many of these same packages' latest versions — has
moved to ESM-only distribution. This is already causing problems: react-docgen v6 (ESM-only)
broke CI because the framework's CJS code can't require() it, requiring an async workaround.

This will keep happening. As more dependencies release ESM-only versions, each one will need
its own async import() workaround, adding complexity to the codebase. Meanwhile, the old
dependency versions pin the framework to outdated APIs — several custom plugins use
remark-parse internals (this.Parser.prototype.blockTokenizers, eat()) that were removed years
ago, and others import private /lib/ paths that could break at any time.

Converting the framework to ESM and upgrading its unified/remark dependencies to current
versions would:

  • Eliminate the CJS/ESM boundary friction for current and future dependencies
  • Move off deprecated, unsupported plugin APIs to the current remark/micromark architecture
  • Remove reliance on private internal imports that could break without notice
  • Align with the direction the Node.js ecosystem is heading, reducing future maintenance
    burden

The tradeoff is a significant one-time migration effort across the markdown processing
pipeline and coordination with 4 consuming repos (patternfly-react, react-topology, chatbot,
react-component-groups) for 2 files each.


Phase 1: Rewrite custom remark plugins (highest risk, do first)

These are the gatekeeping changes. If these can't be ported, the migration is blocked.

1a. Rewrite remove-comments.js (Effort: Small)

  • Currently 22 lines, uses this.Parser.prototype.blockTokenizers + eat() to strip HTML
    comments
  • Migration: Likely redundant — parseMD.js already does remove(tree, 'comment') later in the
    pipeline. May be able to just delete it.

1b. Rewrite auto-link-url.js (Effort: Small-Medium)

  • Currently 33 lines, re-adds https://... autolink support that remark-mdx strips
  • Migration: Check if remark-mdx v3 still strips autolinks. If not, this plugin is
    unnecessary. If still needed, convert to an MDAST tree transform or write a micromark
    extension.

Phase 2: Fix private API dependencies (Medium risk)

2a. Fix mdx-hast-to-jsx.js (Effort: Medium)

  • Uses remark-mdx/lib/serialize/ private internals for serializeTags() and
    serializeMdxExpression()
  • Uses hast-to-hyperscript and @mdx-js/util (v1) which may not exist in v3
  • Uses this.Compiler pattern (removed in unified v11)
  • Need to find public API replacements or inline the functionality

2b. Fix mdx-ast-to-mdx-hast.js (Effort: Small-Medium)

  • Uses mdast-util-to-hast/lib/all private import
  • Handler API may differ in mdast-util-to-hast v13

Phase 3: Update AST node types (Effort: Small)

Rename across 3 files (parseMD.js, mdx-ast-to-mdx-hast.js, mdx-hast-to-jsx.js):

  • mdxBlockElement -> mdxJsxFlowElement
  • mdxSpanElement -> mdxJsxTextElement
  • mdxBlockExpression -> mdxFlowExpression
  • mdxSpanExpression -> mdxTextExpression
  • Check if 'import' node type changed to 'mdxjsEsm' in remark-mdx v3

Phase 4: Convert parseMD.js processing pipeline (Effort: Small)

  • .process(vfile, callback) -> await processor.process(vfile)
  • Makes toReactComponent() async, ripples to sourceMDFile() and processMD()

Phase 5: Upgrade dependencies in package.json (Effort: Small)

Do this AFTER the code changes in phases 1-4.

  • unified: 9.2.2 -> 11.0.5
  • remark-parse: 8.0.3 -> 11.0.0
  • remark-frontmatter: 2.0.0 -> 5.0.0
  • remark-mdx: 2.0.0-next.8 -> 3.1.1
  • remark-mdxjs: 2.0.0-next.8 -> REMOVE (merged into remark-mdx v3)
  • remark-footnotes: 1.0.0 -> 5.0.0
  • remark-squeeze-paragraphs: 4.0.0 -> 6.0.0
  • unist-util-remove: 2.1.0 -> 4.0.0
  • unist-util-visit: 2.0.3 -> 5.1.0
  • to-vfile: 6.1.0 -> 8.0.0
  • vfile-reporter: 6.0.2 -> 8.1.1
  • glob: 12.0.0 -> 13.0.6
  • mdast-util-to-hast: 9.1.2 -> 13.2.1
  • hast-to-hyperscript: 9.0.1 -> 11.0.0
  • @mdx-js/util: 1.6.22 -> REMOVE or replace
  • parse-entities: 2.0.0 -> 3.0.2 (if still needed)
  • detab: 2.0.4 -> 4.0.2

Phase 6: Convert files to ESM (Effort: Medium)

  • Add "type": "module" to package.json
  • Convert all 47 .js files: require() -> import, module.exports -> export
  • Replace __dirname with import.meta.dirname (Node 21+) or
    path.dirname(fileURLToPath(import.meta.url))
  • Replace require.resolve() with import.meta.resolve()
  • Rename consumer-facing files that MUST stay CJS to .cjs: routes.js, routes-client.js,
    routes-generated.js
  • Update webpack alias/resolve config to point to .cjs files
  • CLI entry point (cli.js) can stay as ESM

Phase 7: Test across all consumers (Effort: Medium)

  • patternfly/patternfly (HTML docs, no props)
  • patternfly/patternfly-react (React docs with prop tables — most comprehensive)
  • patternfly/react-topology, patternfly/chatbot, patternfly/react-component-groups
  • Verify: build, start, screenshots, a11y tests

Risk summary:

  • Phase 1-2: Risky, blocking — if these can't be ported, migration is blocked
  • Phase 3-6: Low risk, mostly mechanical
  • Phase 7: Medium risk — integration issues may surface

Jira Issue: PF-3687

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    Status

    Needs triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions