build: migrate from webpack to tsdown#1254
build: migrate from webpack to tsdown#1254christopherthielen wants to merge 3 commits intomasterfrom
Conversation
christopherthielen
commented
Feb 9, 2026
- Replace webpack with tsdown for library bundling
- Output ESM (.mjs) and CJS (.cjs) for bundlers
- Output IIFE bundles for browsers (includes @uirouter/core)
- Remove webpack, ts-loader, cross-env, tsconfig-paths-webpack-plugin
- Remove tweak_sourcemap_paths and fixmaps scripts
- Update package.json exports for dual ESM/CJS
- Modernize tsconfig.json (ES2020 target, bundler resolution)
- Replace webpack with tsdown for library bundling - Output ESM (.mjs) and CJS (.cjs) for bundlers - Output IIFE bundles for browsers (includes @uirouter/core) - Remove webpack, ts-loader, cross-env, tsconfig-paths-webpack-plugin - Remove tweak_sourcemap_paths and fixmaps scripts - Update package.json exports for dual ESM/CJS - Modernize tsconfig.json (ES2020 target, bundler resolution)
There was a problem hiding this comment.
Pull request overview
Migrate the library build pipeline from webpack to tsdown, producing modern ESM/CJS outputs plus browser-friendly IIFE bundles, and updating packaging metadata accordingly.
Changes:
- Remove webpack bundling configuration and dependencies.
- Add
tsdown.config.tsto build ESM/CJS + IIFE (min/non-min) bundles. - Update
package.jsonexports/entrypoints and tighten published files via.npmignore.
Reviewed changes
Copilot reviewed 4 out of 7 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
webpack.config.js |
Removes legacy webpack UMD bundling configuration. |
tsdown.config.ts |
Adds tsdown build definitions for ESM/CJS and browser IIFE outputs. |
package.json |
Updates package entrypoints/exports for dual ESM+CJS and switches build script to tsdown. |
.npmignore |
Adjusts npm publish exclusions for the new toolchain/config files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "type": "module", | ||
| "main": "./dist/index.cjs", | ||
| "module": "./dist/index.mjs", | ||
| "types": "./dist/index.d.mts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.mts", | ||
| "import": "./dist/index.mjs", | ||
| "require": { | ||
| "types": "./dist/index.d.cts", | ||
| "default": "./dist/index.cjs" | ||
| } | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, |
There was a problem hiding this comment.
The published type entrypoints look inconsistent with what most TS declaration emitters produce. dts: true commonly generates *.d.ts (not *.d.mts/*.d.cts), and if those files aren’t actually emitted into dist/, TypeScript consumers will fail to resolve types. Consider updating types/exports.types to point at the actual emitted declaration filenames (often ./dist/index.d.ts), or adjust the build so it truly emits both index.d.mts and index.d.cts.
| "types": "./dist/index.d.mts", | ||
| "import": "./dist/index.mjs", | ||
| "require": { | ||
| "types": "./dist/index.d.cts", | ||
| "default": "./dist/index.cjs" | ||
| } |
There was a problem hiding this comment.
The exports shape is atypical: nesting "types" under "require" is not a standard Node condition and is likely to be ignored by Node, and may not be interpreted by TypeScript the way you intend. Prefer either (a) keep "require" as a string target and provide a top-level "types" target, or (b) use a conditional "types" object keyed by "import"/"require" while keeping runtime "import"/"require" targets as strings.
| "types": "./dist/index.d.mts", | |
| "import": "./dist/index.mjs", | |
| "require": { | |
| "types": "./dist/index.d.cts", | |
| "default": "./dist/index.cjs" | |
| } | |
| "types": { | |
| "import": "./dist/index.d.mts", | |
| "require": "./dist/index.d.cts" | |
| }, | |
| "import": "./dist/index.mjs", | |
| "require": "./dist/index.cjs" |
tsdown.config.ts
Outdated
| external: ['react', 'react-dom', 'react/jsx-runtime'], | ||
| noExternal: ['@uirouter/core', 'prop-types', 'classnames'], | ||
| outDir: 'dist', | ||
| globalName: 'UIRouterReact', | ||
| platform: 'browser', | ||
| outputOptions: { | ||
| globals: { | ||
| react: 'React', | ||
| 'react-dom': 'ReactDOM', | ||
| 'react/jsx-runtime': 'React', | ||
| }, | ||
| }, |
There was a problem hiding this comment.
For the IIFE browser build, mapping 'react/jsx-runtime' to the 'React' global is very likely incorrect: React’s UMD global typically does not expose the jsx/jsxs runtime as React.jsx in a way that satisfies react/jsx-runtime imports. This can lead to runtime failures in the IIFE bundle. Consider bundling react/jsx-runtime into the IIFE outputs (remove it from external for IIFE), or map it to the correct global that the jsx-runtime UMD build provides (and document that requirement for script-tag consumers).
Mapping react/jsx-runtime to the React global doesn't work because
React's UMD global doesn't expose jsx/jsxs as top-level properties.
Instead, bundle the tiny jsx-runtime shim into the IIFE output — its
internal require('react') still resolves to the React global.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BREAKING CHANGE: The IIFE browser bundles (ui-router-react.iife.js and ui-router-react.min.iife.js) are no longer published. The unpkg and jsdelivr package.json fields have been removed. React 19 dropped UMD builds, so there is no longer a React global for the IIFE bundles to reference. All modern React applications use a bundler which consumes the ESM or CJS outputs directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>