Skip to content

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

Merged
mttrbrts merged 7 commits into
accordproject:mainfrom
ujjwalv01:build/esm-exports
Oct 11, 2026
Merged

mttrbrts merged 7 commits into
accordproject:mainfrom
ujjwalv01:build/esm-exports

Conversation

@ujjwalv01

Copy link
Copy Markdown
Contributor

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

ujjwalv01 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @mttrbrts, four choices in this PR are mine and I'd like your call on them before it's merged:

  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
@ujjwalv01

Copy link
Copy Markdown
Contributor Author

Hi @mttrbrts, gentle reminder on this one. The CI workflow is still waiting for maintainer approval (first-time contributor here), so the Build and E2E checks haven't run yet. Could you approve it when you get a chance? Whenever you have time, I'd also like your call on the four questions above, since they decide how I build the next PR in concerto-codegen. Thanks!

Copy link
Copy Markdown
Member

Hi @ujjwalv01 👋 Thank you for this, and welcome to the project! It's a substantial, carefully done PR. The before/after bundle numbers (4.5 MB to 689 KB minified), MIGRATION.md, the ESM smoke checks and the browser e2e suite make it much easier to review. Sorry for the slow reply on CI; I'll get the workflow approved so the Build and E2E checks can run.

Dependencies and supply chain

  • @noble/hashes looks like the right choice to me: it's audited, has no dependencies and no install script, and with it we drop crypto-browserify and its whole asn1/bn.js tree. The tests comparing formula names against Node crypto (non-ASCII, a lone surrogate, long input) are exactly what I'd want before swapping a hash. I'd rather keep it than inline our own SHA-256.
  • esbuild is fine as a root dev dependency, given Concerto already uses it. One nit: e2e/tests/esm-browser.spec.ts imports esbuild, but e2e/package.json doesn't declare it, so it only resolves because npm hoists it to the root. Could you add it to the e2e devDependencies so the dependency is explicit?

Tests
Thanks for folding the UMD cases into the new suite instead of just deleting them. I compared the old specs with the new one, and two assertions didn't make it across:

  • markdown-html.spec.ts checked that ToHtmlStringVisitor is exported. Could you add it to the list in the "exposes the transform API" test (and add it to ENTRY)?
  • The toCiceroMark test used to check that dom.nodes[0].$class is Paragraph. That's a one-line addition.

Smaller things

  • The exports types condition points every caller at the CommonJS lib/index.d.ts. Consumers using moduleResolution: node16/bundler will see CJS types for the ESM entry point. That's probably fine for named exports, but could you run npx @arethetypeswrong/cli --pack on one package to confirm nothing breaks?
  • sideEffects: false is fine as far as I can see: the dayjs.extend(utc) calls sit in modules whose exports are used. Please keep it in mind if anyone adds a module that's imported only for its side effects.
  • Optional: there are leftover umd entries in the .gitignore, .eslintignore and .eslintrc.cjs files. They're harmless and could be removed here or later.

Your four questions (my take):

  1. lib/ vs dist/: keeping lib/ is fine. The exports map makes the folder private, as you say.
  2. Leaving dependencies as imports: agreed. One copy of markdown-it and dayjs is worth requiring a bundler, and the browser consumers we know of all use one.
  3. Removing UMD in this PR: OK with doing it in one PR, since this is going out as 2.0 anyway and MIGRATION.md covers it.
  4. @noble/hashes: yes, see above.

Thanks again! This sets a really nice pattern for the concerto-codegen and cicero-core PRs that follow. 🙏


Generated by Claude Code

Address review: esm-browser.spec.ts imports esbuild, so the e2e package now declares it rather than relying on hoisting. Restore the ToHtmlStringVisitor export check and the Paragraph check in the toCiceroMark test, which did not make it across from the UMD specs.

Signed-off-by: Ujjwal Verma <ujjwalverma010305@gmail.com>
The UMD bundles are no longer built, so nothing writes to umd/ any more.

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

Copy link
Copy Markdown
Contributor Author

Thanks @mttrbrts, and thanks for approving CI! Pushed two commits for the review:

  • esbuild is now declared in e2e/package.json.
  • Restored the two assertions: ToHtmlStringVisitor is in ENTRY and the "exposes the transform API" list, and the toCiceroMark test checks nodes[0].$class is Paragraph.
  • Removed the leftover umd entries from the .gitignore, .eslintignore and .eslintrc.cjs files.

arethetypeswrong (npx @arethetypeswrong/cli --pack, markdown-template): node10, node16 from CJS and bundler are green. node16 from ESM reports "Masquerading as CJS", since types points at the CommonJS declarations. Published @accordproject/concerto-core@5.0.0 gets the identical result. To check it doesn't break anything, I type-checked and ran a "type": "module" project with module: nodenext using named imports, a default import and the markdown-it plugin as a default import: all of them compile and run correctly. If you'd like that flag gone too, I can emit a small lib/esm/index.d.mts for the import condition in a follow-up, and the same fix would then apply to Concerto.

Noted on sideEffects: false. I'll keep side-effect-only modules in mind here and in the concerto-codegen PR.

@mttrbrts
mttrbrts enabled auto-merge (squash) October 11, 2026 10:10
@mttrbrts
mttrbrts merged commit 68c50da into accordproject:main Oct 11, 2026
7 checks passed
@ujjwalv01

Copy link
Copy Markdown
Contributor Author

@mttrbrts thanks for merging #682!

Next in section 2:

  • concerto-codegen feat!: publish a browser ES module build with an exports map concerto-codegen#284 is ready for review, with your two points addressed (inferModel as a root export, esbuild aligned). Its CI workflows still need your approval.
  • cicero-core is next. It needs the markdown packages on 2.0, so could you publish a 2.0.0 release of markdown-transform when you get a chance? I'll start preparing the cicero-core PR in the meantime and switch it to ^2.0.0 once it's on npm.

mttrbrts pushed a commit to accordproject/template-archive that referenced this pull request Oct 11, 2026
… drop the UMD bundle (#954)

* refactor(cicero-core): import util.promisify instead of requiring it

An ES module build cannot follow a runtime require(); esbuild would emit a __require that throws in browsers and Node ES modules.

Refs accordproject/template-engine#185

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

* refactor(cicero-core): hash with @noble/hashes and import Buffer explicitly

Node's crypto and the global Buffer do not exist in browsers. SHA-256 now uses @noble/hashes (already installed through markdown-template 2.0), Buffer comes from the buffer package, now a runtime dependency, and JSZip reads and writes Uint8Arrays. Hashes and archive bytes are unchanged.

Refs accordproject/template-engine#185

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

* feat(cicero-core): publish ES module builds with an exports map

Adds lib/esm (Node import) and lib/esm-browser (browser condition) built with esbuild, following accordproject/markdown-transform#682. Dependencies stay bare imports; the browser build stubs Node builtins except the buffer polyfill. Adds an explicit exports map, module and sideEffects: false, an ESM smoke test and a CI step that runs it.

Refs accordproject/template-engine#185

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

* test(e2e): bundle cicero-core through the browser condition and run it in Chromium

esbuild bundles the lib/esm-browser build the way a web application would. Covers archive loading, signature verification, wrong passphrases, browser hashes matching Node's, logos as Buffers and toArchive in the browser. Declares esbuild in the e2e package.

Refs accordproject/template-engine#185

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

* feat(cicero-core)!: drop the UMD bundle

The exports map routes webpack 5, Vite, Rollup and esbuild to lib/esm-browser, so the UMD bundle only served script-tag users and tools that do not read exports. Remove it with its webpack config, the browser field and the webpack and Node polyfill devDependencies (110 packages out of the lockfile). The UMD e2e cases move into esm-browser.spec.ts. MIGRATION.md describes the changes.

BREAKING CHANGE: umd/cicero-core.js is no longer published and deep imports no longer resolve; bundle the package instead (see MIGRATION.md).

Refs accordproject/template-engine#185

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

* chore(cicero-core): remove the stale nested package-lock.json

The workspace is installed from the root package-lock.json; this leftover still listed the webpack toolchain removed in this PR.

Refs accordproject/template-engine#185

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

---------

Signed-off-by: Ujjwal Verma <ujjwalverma010305@gmail.com>
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.

2 participants