Tooling

Shipping a custom Gutenberg block with TypeScript and esbuild

A lean build setup that respects WordPress bundled dependencies and keeps your sanity intact. Config, externals, aliases, and the dev loop.

esbuild.config.mjs
export default {
  external: [
    'react',
    'react-dom',
    '@wordpress/blocks',
    '@wordpress/block-editor',
    '@wordpress/components',
    '@wordpress/element',
    '@wordpress/i18n',
  ],
};

Every Gutenberg block tutorial shows @wordpress/scripts and a block.json with "editorScript": "file:./build/index.js". That is a good default until you are maintaining a block library, an admin React app, and a front-end script that all need the same TypeScript types and the same externals list. At that point webpack’s abstraction stops feeling free and starts feeling like something you debug on Fridays.

esbuild is my lean alternative for block packages that need speed, explicit control, and a config file small enough to read in one sitting. You trade some magic for clarity: you list WordPress script handles as externals, you decide entry points, and you wire block.json to the output paths yourself.

The one rule that matters: do not bundle React twice

WordPress ships React in the editor as @wordpress/element. If your bundle includes another copy, hooks break in ways that look random. Mark React-related imports as external and map them to globals WordPress registers, or use the @wordpress/* package names directly as externals so esbuild leaves import statements for WordPress to resolve at enqueue time.

A typical externals block in esbuild config looks like this:

external: [
	'react',
	'react-dom',
	'@wordpress/blocks',
	'@wordpress/block-editor',
	'@wordpress/components',
	'@wordpress/element',
	'@wordpress/i18n',
],

Your block.json still declares "editorScript": "file:./build/index.js", and WordPress handles dependency ordering through the asset file generated when you register scripts properly. If you skip the .asset.php companion file, dependency order breaks silently. Generate it in your build script or copy the pattern from @wordpress/scripts output.

Entry points: editor, save, view

Split entries the way WordPress expects. Editor code belongs in the script loaded only in the block editor. Save markup stays in save.js or a static save function if you can keep it dumb. Front-end behavior, if any, goes in a viewScript entry that loads on the public site.

esbuild multi-entry config keeps those graphs separate so you do not ship block editor code to visitors:

entryPoints: {
	'index': 'src/index.tsx',
	'view': 'src/view.tsx',
},
outdir: 'build',
format: 'esm',
bundle: true,

Use format: 'iife' only when you must attach to window for legacy enqueue patterns. ESM works well with modern WordPress script modules where supported; know your client’s hosting and WordPress version before you commit.

TypeScript without waiting on esbuild to typecheck

esbuild transpiles TypeScript quickly because it ignores types. Keep it that way. Run tsc --noEmit in dev watch or CI for type errors. Your editor still gives red squiggles; your build stays under a second for incremental rebuilds.

Shared types for attributes, REST responses, and block props live in a src/types folder imported across editor and admin packages. When attributes change, TypeScript fails in the editor component before you discover the mismatch in manual QA.

Aliases and monorepo paths without pain

Block libraries often share UI components with an admin app in the same repository. esbuild alias maps @ui/button to a package path. Keep aliases mirrored in tsconfig.json paths so imports resolve the same way for tsc and esbuild.

If you use npm workspaces, build packages in dependency order or use a task runner that watches upstream packages. Nothing is worse than editing a shared component and wondering why the block bundle did not change because the watch task only monitors one folder.

block.json, i18n, and asset registration

Run @wordpress/i18n string extraction on your source the way you would with any WordPress JavaScript. esbuild does not replace wp i18n make-json. Script translations still depend on correct text domains and hashed JSON files in languages/.

Register blocks with register_block_type pointing at the folder containing block.json. Let WordPress read metadata and enqueue handles. Avoid manually enqueueing editor scripts unless you have a specific reason, because you will drift from the dependency graph WordPress builds from block.json.

Local dev loop that feels tolerable

Watch mode with esbuild rebuilds on save in milliseconds. Pair it with wp-env or your local site of choice and refresh the editor. For faster feedback, use the Site Editor or a dedicated test post template so you are not navigating through production-like content every time you tweak spacing.

Source maps should be on in development and off or external in production builds you ship to clients. Debugging minified block code without maps is a special kind of pain when the bug only shows up with certain block attributes.

Production builds: minify, tree shake, and sanity checks

Enable minification for production. esbuild’s tree shaking removes unused exports when you use ESM and side-effect-free packages. Verify bundle size after adding a charting library or rich text helper. The editor already loads a lot; your block should not add another full UI framework if @wordpress/components covers the need.

Before tagging a release, smoke test in a clean WordPress install with only your plugin and a default theme. Conflicts from duplicate handles or wrong externals show up immediately there in a way they might not in a dev site loaded with forty plugins.

When esbuild is the right tool

Choose esbuild when you want explicit externals, multiple entry points, and fast rebuilds across a monorepo-style plugin. Stick with @wordpress/scripts when you want zero config and a single block with standard tooling. The goal is not to impress anyone with your build chain. The goal is to ship blocks that load the right scripts on the right screens and never bundle a second React.

Once the pipeline is boring, you spend time on block UX instead of webpack errors, which is where the value actually is.

Earle

Senior WordPress Developer · Sarasota, FL

I've spent 20 years making WordPress behave like a real application platform. I write here about the patterns that survive production and flag the ones I ended up ripping out after they looked fine in a demo.