2026年11月14日

2026年11月14日

npm Workspaces Monorepo for WordPress Block Plugins and Shared Code

This article is a localized edition of the Japanese original. Read the Japanese original

npm workspaces is an npm feature (npm 7 and later) that lets you manage several npm packages in one repository from the root package.json. In WordPress development it helps when you:

  • Want to reuse the same JavaScript functions or components across several block plugins
  • Want dependency installs and the lockfile in one place
  • Want to build every plugin with a single command

This article creates one package for shared code and one block plugin that uses it, then builds with @wordpress/scripts and produces a distributable ZIP. The commands were run and checked in a Node.js 22 (npm 10) Docker container (as of November 2026).

The layout we’ll build

my-wp-monorepo/
├── package.json          ← root (defines workspaces)
├── package-lock.json     ← a single lockfile at the root
├── node_modules/         ← dependencies are installed here together
├── packages/
│   └── shared/           ← shared code (@myorg/shared)
│       ├── package.json
│       └── index.js
└── plugins/
    └── my-block/         ← block plugin (my-block)
        ├── package.json
        ├── my-block.php
        └── src/
            ├── block.json
            └── index.js

Prerequisites

  • Node.js and npm installed. Each @wordpress/scripts release supports specific Node.js versions. As of November 2026, version 37.1.0 supports Node.js 22.22.2 or later (22.x), 24.15.0 or later (24.x), and 26 or later.
  • A WordPress environment to try the built plugin. The block in this article uses apiVersion 3, so it targets WordPress 6.3 or later.
node -v
npm -v

Step 1: Create the root and define workspaces

mkdir my-wp-monorepo && cd my-wp-monorepo
npm init -y
npm pkg set private=true --json
npm pkg set 'workspaces[0]=packages/*' 'workspaces[1]=plugins/*'

The root package.json ends up like this (some fields added by npm init are omitted).

{
  "name": "my-wp-monorepo",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "packages/*",
    "plugins/*"
  ]
}

"private": true keeps you from accidentally publishing the root package to npm. Under workspaces, list the folders to treat as workspaces using patterns. If you manage themes in the same repository, add themes/*.

Step 2: Create the workspaces

Adding -w (--workspace) to npm init creates a package.json in that folder and registers it as a workspace.

# Shared code, named @myorg/shared
npm init -y -w packages/shared --scope=@myorg

# Block plugin, named my-block to match the plugin folder name
npm init -y -w plugins/my-block

# Write the shared code as an ES module
npm pkg set type=module -w packages/shared

# Remove the test script npm init adds, since we don't use it
npm pkg delete scripts.test -w packages/shared -w plugins/my-block

There's a reason not to give the plugin package a scope such as @myorg/. wp-scripts plugin-zip, which we use later, derives the ZIP name and the main PHP file name (my-block.php) from the package name. With a scoped name, the main PHP file was left out of the ZIP. Keeping the package name, folder name and main file name the same makes the plugin easier to handle.

Write the shared code in packages/shared/index.js:

export function formatPrice( value ) {
	return value.toLocaleString( 'ja-JP' ) + '円';
}

Step 3: Reference the shared code from the plugin

When you npm install a workspace by name, npm uses the package in the repository rather than the registry.

# Add @myorg/shared to my-block's dependencies
npm install @myorg/shared -w plugins/my-block

# Add the build tool to my-block's devDependencies
npm install @wordpress/scripts --save-dev -w plugins/my-block

"@myorg/shared": "^1.0.0" is added to dependencies in plugins/my-block/package.json, and node_modules/@myorg/shared at the root becomes a symlink to packages/shared. Changes to the shared code show up in the plugin immediately, without reinstalling.

Dependencies such as @wordpress/scripts are generally installed together in the root node_modules. Only when a workspace needs a different version is it installed inside that workspace.

Register the build scripts in the plugin.

npm pkg set scripts.build="wp-scripts build" scripts.start="wp-scripts start" scripts.plugin-zip="wp-scripts plugin-zip" -w plugins/my-block

Step 4: Prepare the block plugin files

plugins/my-block/src/block.json:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "myorg/price",
	"title": "Price",
	"category": "widgets",
	"editorScript": "file:./index.js"
}

plugins/my-block/src/index.js:

import { registerBlockType } from '@wordpress/blocks';
import { formatPrice } from '@myorg/shared';
import metadata from './block.json';

registerBlockType( metadata.name, {
	edit: () => <p>{ formatPrice( 1980 ) }</p>,
	save: () => <p>{ formatPrice( 1980 ) }</p>,
} );

@wordpress/blocks isn't installed, but wp-scripts build rewrites it to use WordPress core's wp-blocks script, so it builds as-is.

plugins/my-block/my-block.php (the plugin's main file):

<?php
/**
 * Plugin Name: My Block
 * Description: Sample block managed with npm workspaces.
 * Version:     1.0.0
 * Requires at least: 6.5
 * Text Domain: my-block
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

add_action( 'init', function () {
	register_block_type( __DIR__ . '/build' );
} );

Step 5: Build

# Build a single workspace (specify it by name or by path)
npm run build -w my-block

# Run build in every workspace (skipping those without a build script)
npm run build --workspaces --if-present

The build creates these files in plugins/my-block/build/:

File Contents
index.js src/index.js bundled with the shared code it uses (formatPrice)
index.asset.php PHP that returns the scripts it depends on (such as wp-blocks) and a version for cache busting
block.json A copy of src/block.json, read by register_block_type()

During development, npm run start -w my-block rebuilds every time you save a file.

Put a wrapper script in the root package.json and you only need to type npm run build at the root.

npm pkg set scripts.build="npm run build --workspaces --if-present"

Use npm ls to check that the dependencies are linked.

$ npm ls @myorg/shared
my-wp-monorepo@1.0.0 /work/my-wp-monorepo
+-- @myorg/shared@1.0.0 -> ./packages/shared
`-- my-block@1.0.0 -> ./plugins/my-block
  `-- @myorg/shared@1.0.0 deduped -> ./packages/shared

Step 6: Create a distributable ZIP

npm run plugin-zip -w my-block

This creates plugins/my-block/my-block.zip, containing my-block/my-block.php and the three files in my-block/build/. The shared code is bundled into build/index.js, so the plugin you install in WordPress doesn't need node_modules or any other folder from the monorepo.

To choose which files go into the ZIP yourself, add files to the plugin's package.json (for example, "files": ["build", "my-block.php", "readme.txt"]).

To try it in a local WordPress, put the plugins/my-block folder in WordPress's wp-content/plugins/ or create a symlink to it, then activate the plugin in the admin.

Day-to-day tips

  • One lockfile at the root. package-lock.json is created only at the root. Commit it, and in CI run npm ci at the root to install the dependencies of every workspace.
  • Add dependencies from the root with -w. The command records which workspace got what, and you can paste it straight into docs or CI.
  • Changes to shared code affect every plugin. After changing the shared package, rebuild all plugins with npm run build --workspaces --if-present and check them.
  • Decide whether to commit build output. If build/ isn't in the repository, build as part of deployment before creating the ZIP.

To get started with block development itself, see also Scaffolding a block with create-block.

Summary

  • Put "private": true and workspaces in the root package.json, and create workspaces with npm init -w
  • npm install @myorg/shared -w plugins/my-block references the shared package in the repository through a symlink
  • build from @wordpress/scripts bundles the shared code into the plugin's build/index.js
  • Build everything with npm run build --workspaces --if-present and package with npm run plugin-zip -w my-block
  • Don't scope the plugin's package name; match it to the folder name and main file name

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲