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

    No fields configured for Epic.

    Projects

    Status

    Needs triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions