This repository uses npm workspaces to manage WordPress packages and lerna to publish them with to npm.
Packages are the first layer of architecture and organization in Gutenberg. They exist to separate concerns, provide clarity, and establish a shared mental model across teams. To maintain good package hygiene, follow these guidelines when creating new packages or iterating on existing ones:
-
Each package should have a single, clear purpose.
It should be immediately obvious why the package exists.
-
Every package must include a README.
This is the first place contributors look to understand scope and usage.
-
Any prerequisites must be documented.
Generic packages without prerequisites are better, but packages with some prerequisites are acceptable. Examples of prerequisites: API endpoints that must exist, authentication assumptions, environment dependencies. These should be clearly stated in the README.
-
Public APIs should have documentation.
Either inline in the README or linked to external docs.
-
Avoid utility and kitchen-sink packages.
They tend to grow without a coherent domain and become junk drawers.
-
Avoid broad, catch-all scopes.
For example: "Reusable WordPress components" or "Utilities for different use cases." These create unclear ownership and encourage uncontrolled growth. Instead, define a specific domain or purpose.
-
Default to bundled packages (no globals, no modules) unless necessary.
In Gutenberg, we should default to "bundled" packages unless there's a specific need for globals or modules. See the @wordpress/build README for more information on package configuration.
For more information on the build system and package configuration, see the @wordpress/build README.
When creating a new package, you need to provide at least the following. Packages bundled in Gutenberg or WordPress must include a wpScript and or wpScriptModuleExports field in their package.json file. See the details below.
-
package.jsonbased on the template:This assumes that your code is located in the
srcfolder and will be transpiled by the build system.For production packages that will ship as a WordPress script, include
wpScript: truein thepackage.jsonfile. This tells the build system to bundle the package for use as a WordPress script.For production packages that will ship as a WordPress script module, include a
wpScriptModuleExportsfield in thepackage.jsonfile. The value of this field can be a string to expose a single script module, or an object with a shape like the standardexportsobject to expose multiple script modules from a single package:{ "name": "@wordpress/example", // The string form exposes the `@wordpress/example` script module. "wpScriptModuleExports": "./build-module/index.js", // Multiple sub-modules can be exposed by providing an object: "wpScriptModuleExports": { // Exposed as `@wordpress/example` script module. ".": "./build-module/index.js", // Exposed as `@wordpress/example/demo-block/view` script module. "./demo-block/view": "./build-module/index.js" } }Both
wpScriptandwpScriptModuleExportsmay be included if the package exposes both a script and a script module. These fields are also essential when performing a license check for all their dependencies, because they trigger strict validation against compatibility with GPL v2. All remaining dependencies WordPress doesn't distribute but uses for development purposes can contain also a few other OSS compatible licenses.For more details on package configuration options, see the @wordpress/build README.
-
README.mdfile containing at least:- Package name
- Package description
- Installation details
- Usage example
- API documentation, if applicable (more info)
- A link to the contributing guidelines (here's an example from the a11y package)
Code is Poetrylogo (<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>)
-
CHANGELOG.mdfile containing at least:<!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. --> ## Unreleased Initial release.
To ensure your package is recognized in npm workspaces, you should run npm install to update the package lock file.
By default, packages do not expose as WordPress scripts/modules (not accessible via the wp global). Only packages that should be directly available in WordPress should set wpScript: true.
Omit wpScript (or explicitly set to false) for packages designed solely as dependencies for other packages:
{
"wpScript": false
}Examples of packages that should not expose to the wp global:
- Utility packages used internally by other packages
- Shared logic or helpers without a direct WordPress use case
- Intermediate packages intended only as dependencies of other
@wordpress/*packages
When a package omits wpScript or sets it to false, it:
- Will not be exposed as a WordPress script (not available via the
wpglobal) - Can still be used as a dependency by other packages (via npm imports)
- Should still be published to npm to support backporting to WordPress core releases
In rare cases, if a package is only used internally within Gutenberg and should never be published to npm, mark it as private:
{
"private": true,
"wpScript": false
}Private packages will be excluded from npm publication and should only be used for development-only utilities (such as build tools or internal development helpers). They should not be used as dependencies for other packages, as this could break the backporting process to WordPress core.
Note: You can safely include the publishConfig field in private packages—it will be ignored by npm since the private flag takes precedence.
There are two types of dependencies that you might want to add to one of the existing WordPress packages.
Production dependencies are stored in the dependencies section of the package’s package.json file.
The simplest way to add a production dependency to one of the packages is to run a command like the following from the root of the project.
Example:
npm install change-case -w packages/a11yThis command adds the change-case as a dependency to the @wordpress/a11y package, which is located in packages/a11y folder. If there was the same dependency installed then the version specified in the package-lock.json file is going to be reused. If you want to enforce a different version, you can do so by adding the @ suffix to the package name.
Example:
npm install change-case@latest -w packages/a11yRemoving a dependency from one of the WordPress packages is similar to installation. You need to run a command like the following from the root of the project.
Example:
npm uninstall change-case -w packages/a11yThis is the most confusing part of working with [monorepo] which causes a lot of hassles for contributors. The most successful strategy so far is to do the following:
- First, remove the existing dependency as described in the previous section.
- Next, add the same dependency back as described in the first section of this chapter. This time it will get the latest version applied unless you enforce a different version explicitly.
In contrast to production dependencies, development dependencies shouldn't be stored in individual WordPress packages. Instead they should be installed in the project's package.json file using the usual npm install command. In effect, all development tools are configured to work with every package at the same time to ensure they share the same characteristics and integrate correctly with each other.
Example:
npm install glob --save-devThis commands adds the latest version of glob as a development dependency to the package.json file. It has to be executed from the root of the project.
Each public API change needs to be reflected in the corresponding API documentation. To ensure that code and documentation are in sync automatically, Gutenberg has developed a few utilities.
Packages can add the following HTML comment within their top-level README.md:
<!-- START TOKEN(Autogenerated API docs) -->
Content within the HTML comment will be replaced by the generated documentation.
<!-- END TOKEN(Autogenerated API docs) -->`.Each time there is a commit to the public API of the package the README.md will be updated and kept in sync.
The above snippet within the package's README.md signals the Gutenberg utilities to go to src/index.js and extract the JSDoc comments of the export statements into a more friendly format.
Packages may want to use a different source file or add the exports of many files into the same README.md (see packages/core-data/README.md as an example); they can do so by adding the relative path to be used as source into the HTML comment:
<!-- START TOKEN(Autogenerated API docs|src/actions.js) -->
Content within the HTML comment will be replaced by the generated documentation.
<!-- END TOKEN(Autogenerated API docs|src/actions.js) -->`.It's very important to have a good plan for what a new package will include. All constants, methods, and components exposed from the package will ultimately become part of the public API in WordPress core (exposed via the wp global - eg: wp.blockEditor) and as such will need to be supported indefinitely. You should be very selective in what is exposed by your package and ensure it is well documented.
A plugin can bundle one @wordpress/* package while loading its dependencies from WordPress. The bundled package and its WordPress dependencies can then come from different releases.
For example, a plugin might bundle a newer @wordpress/dataviews package but run on a WordPress version that provides an older @wordpress/components package. The reverse can also happen: an older plugin bundle can run on a newer WordPress version.
Before changing an API or dependency in this setup:
- Confirm which packages the application bundles and which ones WordPress supplies.
- Test both mixed-version combinations: the new bundle with each supported WordPress version, and older supported bundles with the new WordPress package.
- Keep production public APIs compatible. Follow the backward compatibility policy if a break is unavoidable.
- Do not use private APIs in bundled packages. Private APIs can be removed, but first check that supported bundles no longer depend on them.
- Test the built package, not only its source. The built result can load dependencies and shared state differently.
See Testing published packages across WordPress versions for the test matrix and release procedure.
Each package keeps a CHANGELOG.md so the release process can see what changed since the last publish. Put new entries under ## Unreleased at the top of the file, or create the heading if it is missing.
Prefer an entry for anything that affects package consumers. Changelog entries are optional for trivial or for changes that don't impact the user or consumer. Internal changes can be added under an Internal heading.
Under the "Unreleased" heading, add entries as list items under an appropriate ### subheading (see "Changelog Subsections"). Each top-level bullet should end with a link to the pull request and a period.
Example:
<!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. -->
## Unreleased
### Bug Fixes
- Fixed an off-by-one error with the `sum` function ([#12347](https://github.com/WordPress/gutenberg/pull/12347)).You can verify the structure locally by running npm run lint:changelogs.
The automated package publishing workflow will at most bump the minor version of a pre-release package (those having a version like 0.x.x), even if it includes breaking changes. This is consistent with semantic versioning, where 0.x versions are intended for initial development where the API may change frequently.
A 0.x package is promoted to 1.0.0 by adding a Stable Release section.
## Unreleased
### Stable Release
This package is now considered stable and production-ready. The API will follow semantic versioning from this point forward.
### Breaking Changes
- Final API adjustments before 1.0.0 release ([#12345](https://github.com/WordPress/gutenberg/pull/12345)).The presence of the "Stable Release" heading will cause the automated release process to bump a pre-1.0 package to 1.0.0. The "Stable Release" heading should only be used for pre-1.0 packages, and from that point forward breaking changes will result in major version bumps as expected.
There are a number of common release subsections you can follow. Each is intended to align to a specific meaning in the context of the Semantic Versioning (semver) specification the project adheres to. It is important that you describe your changes accurately, since this is used in the packages release process to help determine the version of the next release.
Use the following standardized section headings and ordering based on the type of change you are making:
- "Stable Release" - Marks a pre-1.0 package as stable and production-ready. This should only be used for packages currently published as a 0.x pre-release, to intentionally communicate that a package's API is now stable and ready for production use.
- "Breaking Changes" - A backwards-incompatible change which requires specific attention of the impacted developers to reconcile (requires a major version bump for stable packages).
- "New Features" - The addition of a new backwards-compatible function or feature to the existing public API (requires a minor version bump).
- "Enhancements" - Backwards-compatible improvements to existing functionality (requires a minor version bump).
- "Deprecations" - Deprecation notices. These do not impact the public interface or behavior of the module (requires a minor version bump).
- "Bug Fixes" - Resolutions to existing buggy behavior (requires a patch version bump).
- "Internal" - Changes which do not have an impact on the public interface or behavior of the module (requires a patch version bump).
- "Documentation" - Changes affecting consumer documentation.
If you are publishing new versions of packages, note that there are versioning recommendations outlined in the Gutenberg Release Process document which prescribe minimum version bumps for specific types of releases. The chosen version should be the greater of the two between the semantic versioning and Gutenberg release minimum version bumps.
The TypeScript language is a typed superset of JavaScript that compiles to plain JavaScript.
Gutenberg uses TypeScript for several reasons, including:
- Powerful editor integrations improve developer experience.
- Type system can detect some issues and lead to more robust software.
- Type declarations can be produced to allow other projects to benefit from these advantages as well.
Gutenberg uses TypeScript by running the TypeScript compiler (tsc) on select packages.
These packages benefit from type checking and produced type declarations in the published packages.
A package opts in to TypeScript tooling with a build project registered in the root tsconfig.build.json references: tsconfig.json for a package without TypeScript dev files, tsconfig.build.json for one that splits. Packages that emit declarations through this standard layout and have TypeScript test or story files split into two projects:
tsconfig.build.jsonis the build project: it coverssrc, emits declarations tobuild-types, and is what other packages andnpm run buildconsume.npm run buildemits those declarations with--noCheck, so it only reports parse and declaration emit errors;npm run typecheckis where type errors surface.tsconfig.jsonis the dev project: it covers test and story files withnoEmit, sonpm run typecheckand the IDE can check them without their declarations ending up in the published package.
Both extend shared base configurations (comments are not necessary):
// tsconfig.build.json
{
// Extends a base configuration common to most packages.
"extends": "@wordpress/monorepo-tools/tsconfig/base.json",
// Dependencies that have opted in to TypeScript are referenced here: a
// split one by its build project, one on a single config by its
// directory.
"references": [
{ "path": "../dom-ready/tsconfig.build.json" },
{ "path": "../hooks" }
]
}// tsconfig.json
{
// Extends the shared dev project configuration (noEmit, Vitest matcher types,
// test and story includes).
"extends": "@wordpress/monorepo-tools/tsconfig/dev.base.json",
// The dev project checks against the build project's declarations.
"references": [ { "path": "./tsconfig.build.json" } ]
}Register both projects at the root: packages/<name>/tsconfig.build.json in the root tsconfig.build.json references, and packages/<name> in the root tsconfig.json references. Route entry points under routes/ and widgets under widgets/ with a tsconfig.json register it in the root tsconfig.json references only: their projects emit nothing and nothing else references them, so that registration is what puts them under npm run typecheck. An entry with TypeScript test files pairs it with a tsconfig.test.json covering them, registered the same way.
Packages whose components feed the Storybook components manifest (components, dataviews, ui) carry a third project, tsconfig.stories.json, registered in the root tsconfig.json only. It type checks the stories against component sources without test types. Storybook's component meta extractor reads props through the closest tsconfig.json that lists a story, or through its own inferred project when none does; the inferred project produces the complete manifest and the dev project does not, so stories stay out of tsconfig.json. The dev project cannot reference this one either, because a referenced project may not disable emit (TS6310), which is why it is registered at the root only.
Two rules keep the projects consistent, and npm run lint:tsconfig enforces both:
- The build project excludes every dev file (
**/test/**,**/tests/**,**/__tests__/**,**/stories/**,**/*.story.*) and never lists a test type such asgutenberg-vitest-test-envorvitest/globalsintypes, sosrccannot use test globals and no dev declaration is published. A packageexcludereplaces the inherited one, so list all of them. - The dev project's
typesstarts from the build project's list and addsgutenberg-vitest-test-env, so tests see every ambient type the sources see. Ambient types only dev files need (@types/node,@testing-library/jest-dom) belong in the package's owndevDependencies.
A few packages emit declarations through a different layout and keep only the parts of the split that apply. interactivity-router pairs its dev project with two specialized build projects (tsconfig.main.json and tsconfig.full-page.json), which take the standard build project's place in the root tsconfig.build.json references. The rules above still apply to whichever projects such a package has.
The build project inherits rootDir, declarationDir, and include from the base configuration, so a package only sets what differs. Test files that do not type check yet are listed in the dev project's exclude with a comment, so the debt stays visible per file.
Type declarations will be produced in the build-types which should be included in the published package.
For consumers to use the published type declarations, we'll set the types field in package.json:
{
"main": "build/index.js",
"module": "build-module/index.js",
"types": "build-types"
}Ensure that the build-types directory will be included in the published package, for example if a files field is declared.
WordPress packages adhere the Node.js Release Schedule. Consequently, the minimum required version of Node.js is specified using the engines field in package.json for all packages. This ensures that production applications run only on Active LTS or Maintenance LTS releases on Node.js. LTS release status is "long-term support", which typically guarantees that critical bugs will be fixed for a total of 30 months.
In order for bundlers to tree-shake packages effectively, they often need to know whether a package includes side effects in its code. This is done through the sideEffects field in the package's package.json.
If your package has no side effects, simply set the field to false:
{
"name": "package",
"sideEffects": false
}If your package includes a few files with side effects, you can list them instead:
{
"name": "package",
"sideEffects": [
"file-with-side-effects.js",
"another-file-with-side-effects.js"
]
}Please consult the side effects documentation for more information on identifying and declaring side effects.
Publishing WordPress packages to npm is automated by synchronizing it with the bi-weekly Gutenberg plugin RC1 release. You can learn more about this process and other ways to publish new versions of npm packages in the Gutenberg Release Process document.
{ "name": "@wordpress/package-name", "version": "1.0.0-prerelease", "description": "Package description.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ "wordpress" ], "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/package-name/README.md", "repository": { "type": "git", "url": "https://github.com/WordPress/gutenberg.git", "directory": "packages/package-name" }, "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" }, "engines": { "node": ">=18.12.0" }, "main": "build/index.js", "module": "build-module/index.js", // Include this line to include the package as a WordPress script. "wpScript": true, // Include this line to include the package as a WordPress script module. "wpScriptModuleExports": "./build-module/index.js", "types": "build-types", "sideEffects": false, "publishConfig": { "access": "public" } }