Skip to content

feat(*)!: publish ES module builds with exports maps and drop the UMD bundles - #682

Open
ujjwalv01 wants to merge 5 commits into
accordproject:mainfrom
ujjwalv01:build/esm-exports
Open

ujjwalv01 wants to merge 5 commits into
accordproject:mainfrom
ujjwalv01:build/esm-exports

Conversation

@ujjwalv01

Copy link
Copy Markdown

Part of accordproject/template-engine#185 (section 2)

Every library package now ships two ES module builds next to the CommonJS lib/: lib/esm for Node and lib/esm-browser for bundlers targeting the web. An explicit exports map selects between them. The webpack UMD bundles are removed. A browser bundler now takes each package's lib/esm-browser and shares one copy of markdown-it, dayjs and Concerto with the rest of the application. Before, each UMD bundle carried its own copy of Concerto plus about 1 MB of Node polyfills, and the markdown-transform UMD nested the markdown-template and markdown-html UMDs inside itself. The approach follows Concerto v5 (accordproject/concerto#1306).

What template-engine imports from these packages ({ TemplateMarkTransformer } and { transform }), bundled for the browser with esbuild, Concerto included:

Minified Gzipped
1.1.0, through the UMD bundles 4,571 KB 1,341 KB
This PR, through lib/esm-browser 689 KB 192 KB

Changes

  • refactor(*): the five remaining require() calls in src/ become import statements: markdown-it-cicero, markdown-it-template, concerto-core's ModelLoader, dijkstrajs and type-of. Type declarations are added for the last two. An ES module build cannot follow a require(); the one in markdown-transform would have thrown in browsers as soon as the module loaded. dayjs/plugin/utc becomes dayjs/plugin/utc.js, because dayjs has no exports map and Node ESM needs the extension.

  • feat(markdown-template): formulaName() uses @noble/hashes instead of Node crypto. Node crypto was the only reason crypto-browserify was in the browser bundles. Formula names are unchanged; new tests compare them against Node crypto.

  • feat(*)!: scripts/build-esm.js emits lib/esm and lib/esm-browser with esbuild, one output module per source module. Dependencies stay as imports; the browser build replaces Node builtins and jsdom with empty modules. The seven library packages get exports (. and ./package.json only), module and sideEffects: false. markdown-cli is a Node-only CLI and is unchanged. scripts/smoke-esm.mjs runs after the root build with 9 checks:

    • Node resolves import to lib/esm;
    • ESM and CJS export the same names;
    • real transforms work through the ESM build;
    • neither build keeps a runtime require() (other than jsdom in the Node build);
    • the browser build has no Node builtins and no process.
  • test(e2e): esm-browser.spec.ts bundles the packages through the browser condition, as an application's bundler would, and runs the result in Chromium.

  • feat(*)!: the UMD bundles are dropped, along with:

    • the webpack configs and the browser field;
    • the webpack and polyfill devDependencies and the process dependency (113 packages out of the lockfile);
    • the CI "Build UMD bundles" step.

    The UMD e2e cases move into esm-browser.spec.ts. MIGRATION.md is added, and the e2e README, the markdown-html README and .github/copilot-instructions.md are updated.

Flags

  • Breaking, needs a major release (2.0):

    • imports of files inside a package (.../lib/...) no longer resolve;
    • Node import now loads the ESM build, so a process using both import and require() holds two copies;
    • the UMD bundles and the process dependency are gone.

    MIGRATION.md covers each case. I found no deep imports of these packages in other accordproject repos; template-engine, template-playground, cicero-core, template-cli, template-compiler and vscode-extension all import package roots.

  • New dependencies:

    • @noble/hashes ^1.8 (runtime, markdown-template) is a pure JavaScript SHA-256: 5 KB minified, CommonJS and ESM builds. I used v1 because v2 is ESM-only.
    • esbuild ^0.27.7 (root dev dependency) is the same tool Concerto uses for its ESM builds.
  • Differs from Concerto: Concerto bundles third-party dependencies into its browser build; this PR leaves them as imports. That gives one copy of markdown-it instead of three, and 689 KB instead of 977 KB minified for the imports above. The cost is that lib/esm-browser cannot be loaded without a bundler using an import map alone, because dayjs and @xmldom/xmldom are CommonJS.

  • Differs from (feat) convert commonmark dom to plain text #185: item 1 proposed keeping the UMD as a ./umd subpath; this PR removes it, as Concerto v5 did.

  • The design choices above are listed as questions in the first comment.

Screenshots or Video

N/A: build and packaging change.

Related Issues

Testing

  • npm test: 1,866 passed (1,860 before plus 6 new), 1,602 snapshots unchanged; lint shows 0 errors and licchk passes.
  • npm run test:esm: 9/9. I confirmed the guard works: putting one require() back makes it fail in both builds.
  • Browser e2e: 10/10.
  • Run locally on Windows 11 with Node 24.15 and npm 11.12. CI covers Node 22 on Ubuntu, macOS and Windows.

Author Checklist

  • Ensure you provide a DCO sign-off for your commits using the --signoff option of git commit.
  • Vital features and changes captured in unit and/or integration tests
  • Commits messages follow AP format
  • Extend the documentation, if necessary
  • Merging to main from ujjwalv01:build/esm-exports

An ES module build cannot follow a require() call: esbuild keeps it as a runtime require, which throws in browsers (the concerto-core one in markdown-transform runs at load time). Convert the five remaining calls to imports and declare the two untyped modules. dayjs has no exports map, so its plugin subpath carries the .js extension Node ESM needs.

Signed-off-by: Ujjwal Verma <ujjwalverma010305@gmail.com>
formulaName() was the only use of Node crypto, which pulled about 1 MB of crypto-browserify into browser bundles. @noble/hashes 1.x is a pure JavaScript SHA-256 (5 KB minified) with CommonJS and ESM builds. Names are unchanged; new tests compare against Node crypto.

Signed-off-by: Ujjwal Verma <ujjwalverma010305@gmail.com>
scripts/build-esm.js, modelled on Concerto v5, emits lib/esm (import condition) and lib/esm-browser (browser condition) next to the CommonJS lib/. Dependencies stay bare imports so consumers share one copy of each; the browser build stubs Node builtins and jsdom. scripts/smoke-esm.mjs runs after the root build.

BREAKING CHANGE: each package now has an exports map, so deep imports into lib/ no longer resolve, and Node import loads the ESM build.

Signed-off-by: Ujjwal Verma <ujjwalverma010305@gmail.com>
Bundles the packages through the browser export condition with esbuild, as an application would, and runs the result in Chromium.

Signed-off-by: Ujjwal Verma <ujjwalverma010305@gmail.com>
The exports maps already route webpack 5, Vite, Rollup and esbuild to lib/esm-browser, so the UMD bundles only served script-tag users and tools that do not read exports. Remove them with their webpack configs, the browser field, the Node polyfill devDependencies and the process dependency (113 packages out of the lockfile). The UMD e2e cases move into esm-browser.spec.ts. MIGRATION.md describes the 2.0 changes.

BREAKING CHANGE: the umd/markdown-html.js, umd/markdown-template.js and umd/markdown-transform.js bundles are no longer published; bundle the packages instead (see MIGRATION.md).

Signed-off-by: Ujjwal Verma <ujjwalverma010305@gmail.com>
@ujjwalv01

Copy link
Copy Markdown
Author

Hi @mttrbrts , opening this as a draft because four choices here are mine and I'd like your call before marking it ready:

  1. Folder: I kept lib/ (ESM in lib/esm and lib/esm-browser) rather than renaming to dist/. With the exports map the folder is private, so renaming later would break nobody. Happy to switch if you want parity with Concerto.
  2. Dependencies in the browser build: I left them as imports instead of bundling them as Concerto does: 689 KB vs 977 KB minified, and one copy of markdown-it instead of three. The trade-off is that lib/esm-browser needs a bundler (or a CDN that converts CommonJS packages); an import map alone isn't enough. OK with that?
  3. UMD removed, in one PR: (feat) convert commonmark dom to plain text #185 proposed keeping the UMD as a ./umd subpath; I dropped it entirely, as Concerto v5 did, with the removal as the last commit. If you'd rather keep ./umd, I'll revert that commit and add the subpath. If you'd prefer two PRs, that commit is the split point.
  4. @noble/hashes as a new runtime dependency of markdown-template, replacing Node crypto for formula names. Fine, or would you rather have an inlined SHA-256?

This is the first of the section 2 PRs. Next are concerto-codegen (which can go in parallel), cicero-core once markdown 2.0 is released, and then template-engine.

@ujjwalv01
ujjwalv01 marked this pull request as ready for review October 4, 2026 10:20

This branch has not been deployed

No deployments
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.

1 participant