用于 WordPress 插件的构建工具。

描述

@wordpress/build 是一个为 WordPress 插件设计的约定式构建系统。它提供:

  • 转译:使用 esbuild 将 TypeScript/JSX 源代码转换为 CommonJS(build/)和 ESM(build-module/)两种格式
  • 样式编译:处理 SCSS 文件和 CSS 模块,生成 LTR 和 RTL 版本
  • 打包:为 WordPress 脚本和模块创建浏览器可用的包
  • PHP 生成:自动为脚本、模块和样式生成 PHP 注册文件
  • 监听模式:在开发期间进行增量重建

安装

npm install @wordpress/build --save-dev

用法

生产构建

wp-build

或通过 npm 脚本:

{
    "scripts": {
        "build": "wp-build"
    }
}

开发模式(监听)

wp-build --watch

或通过 npm 脚本:

{
    "scripts": {
        "dev": "wp-build --watch"
    }
}

测试生成的样式

当 NODE_ENV 为 test 且 process 已定义时,生成的 CSS 输出会在基于 Node 的环境中跳过自动样式注入。
浏览器没有 process,
因此始终会接收样式。基于 Node 的 DOM 实现(例如 jsdom)
无法可靠支持现代 CSS 特性,因此需要 DOM 中存在实际样式的测试应在浏览器环境中运行。

浏览器支持

JavaScript 和 CSS 构建在指定构建目标时使用 Browserslist。
你可以 自行提供配置,
或者使用默认的 @wordpress/browserslist-config
来遵循 WordPress 浏览器支持。

包配置

使用以下可选字段配置你的 package.json:

wpScript

控制该包是否作为打包后的 WordPress 脚本/模块公开,并可通过配置的全局变量访问。

  • true:该包将被打包并作为 WordPress 脚本公开。它将作为配置的全局变量的一部分在 WordPress 中可用(例如 wp.blockEditor、wp.data,或在配置不同时使用自定义全局名称)。
  • 省略或 false(默认):该包不会作为 WordPress 脚本公开。仅作为其他包依赖项而设计的包应使用此项。该包仍可由其他包通过 npm 导入作为依赖项使用。

{
    "wpScript": true
}

有关何时省略或将此项设置为 false 的更多详情,请参阅 包指南。

wpScriptModuleExports

定义脚本模块入口点:

{
    "wpScriptModuleExports": {
        "./interactivity": "./build-module/interactivity/index.js"
    }
}

wpScriptDefaultExport

处理默认导出包装:

{
    "wpScriptDefaultExport": true
}

wpScriptExtraDependencies

额外的脚本依赖项:

{
    "wpScriptExtraDependencies": [ "wp-polyfill" ]
}

wpStyleEntryPoints

自定义 SCSS 入口点模式:

{
    "wpStyleEntryPoints": {
        "style": "src/style.scss"
    }
}

wpCopyFiles

要复制的文件,可选择进行 PHP 转换:

{
    "wpCopyFiles": [
        {
            "from": "src/index.php",
            "to": "build/index.php",
            "transform": "php"
        }
    ]
}

wpWorkers

为需要自包含 Web Worker 文件的包提供 Worker 打包定义。
Worker 会包含所有依赖项进行打包,并可通过 Blob URL 加载。

字符串简写 —— 仅入口路径:

{
    "wpWorkers": {
        "./worker": "./src/worker.ts"
    }
}

对象格式 —— 带模块解析重定向的入口路径:

{
    "wpWorkers": {
        "./worker": {
            "entry": "./src/worker.ts",
            "resolve": {
                "vips-es6.js": "vips.js"
            }
        }
    }
}

resolve 映射会在打包期间重定向模块加载。键是要匹配的文件名模式;
值是同目录中的替换文件名。
当依赖项的 ES 模块入口使用 import.meta.url 时,这很有用,
该用法在 Blob URL Worker 环境中会失败。通过重定向到替代入口(例如 CommonJS 版本),可以避免此问题。

根配置

使用 wpPlugin 对象配置你的根 package.json,以控制全局命名空间和外部化行为:

wpPlugin.name

用于为生成的 PHP 函数添加前缀的名称。必须遵循 PHP 中的函数名规则,即有效名称以字母或下划线开头,后跟任意数量的字母、数字或下划线。

{
    "wpPlugin": {
        "name": "myPlugin"
    }
}

wpPlugin.scriptGlobal

你的包使用的全局变量名(例如 "wp"、"myPlugin")。设置为 false 可禁用全局暴露:

{
    "wpPlugin": {
        "scriptGlobal": "myPlugin"
    }
}

wpPlugin.packageNamespace

用于匹配全局暴露的包作用域(不带 @ 前缀)。只有匹配 @{packageNamespace}/* 的包会暴露全局变量:

{
    "wpPlugin": {
        "scriptGlobal": "myPlugin",
        "packageNamespace": "my-plugin"
    }
}

wpPlugin.handlePrefix

用于 .asset.php 文件中 WordPress 脚本句柄的前缀(例如 wp-data、my-plugin-editor)。默认值为 packageNamespace:

{
    "wpPlugin": {
        "scriptGlobal": "myPlugin",
        "packageNamespace": "my-plugin",
        "handlePrefix": "mp"
    }
}

使用此配置:

  • @my-plugin/editor → window.myPlugin.editor,句柄为 mp-editor
  • @my-plugin/data → window.myPlugin.data,句柄为 mp-data

wpPlugin.externalNamespaces

要外部化的其他包命名空间(作为外部依赖消费,而不暴露)。每个命名空间必须是一个对象,包含 global 和可选的 handlePrefix:

{
    "wpPlugin": {
        "externalNamespaces": {
            "woo": {
                "global": "woo",
                "handlePrefix": "woocommerce"
            },
            "acme": {
                "global": "acme",
                "handlePrefix": "acme-plugin"
            }
        }
    }
}

这允许你的包将第三方依赖项作为外部依赖消费:

  • import { Cart } from '@woo/cart' → window.woo.cart,句柄为 woocommerce-cart
  • import { Button } from '@acme/ui' → window.acme.ui,句柄为 acme-plugin-ui
  • 依赖项会在 .asset.php 文件中被跟踪

如果省略 handlePrefix,则默认使用命名空间键(例如 "woo" → woo-cart)。

wpPlugin.pages(实验性)

定义支持路由的管理页面。会为每个页面生成用于路由注册的 PHP 函数,并且可由其他插件扩展。

页面可以定义为简单字符串,也可以定义为带初始化模块的对象:

{
    "wpPlugin": {
        "pages": [
            "my-admin-page",
            {
                "id": "my-other-page",
                "init": [ "@my-plugin/my-page-init" ]
            }
        ]
    }
}

页面配置:

  • 字符串格式:"my-admin-page" —— 不带初始化模块的简单页面
  • 对象格式:{ "id": "page-slug", "init": ["@scope/package"], "capability": "edit_theme_options", "experimental": true } —— 带可选初始化模块的页面
    • id(必填):WordPress 后台 URL 中使用的页面别名
    • init(可选):页面初始化期间要执行的脚本模块 ID 数组
    • capability(可选,默认 manage_options):用户查看该页面所需的能力。全页模式会自行强制执行此设置,因为它在菜单页面回调流程之外渲染;请将其与注册该页面菜单条目时使用的能力保持一致。
    • experimental(可选,默认 false):当为 true 时,该页面将从 WordPress Core 构建中排除(IS_WORDPRESS_CORE=true),任何仅属于实验性页面的路由也会一并排除。

生成的文件:

这会生成两种页面模式:

  • build/pages/my-admin-page/page.php – 整页模式(使用自定义侧边栏接管整个管理屏幕)
  • build/pages/my-admin-page/page-wp-admin.php – WP-Admin 模式(集成在标准 wp-admin 界面中)
  • build/pages.php – 所有页面的加载器

每种模式都提供路由/菜单注册函数和渲染回调。路由会自动为两种模式注册。

启动模块:

页面通过 @wordpress/boot 脚本模块启动,该模块随 WordPress Core 7.0+ 和 Gutenberg 插件一起提供。插件不需要自己的 packages/boot。

生成的页面文件会优先使用插件本地的 build/modules/boot/index.min.asset.php(如果存在),否则使用 Core 中位于 wp-includes/js/dist/script-modules/boot/ 的副本。如果两者都不可用,页面将不会加载任何脚本,并只显示“此屏幕需要 JavaScript”提示。

为 WP-Admin 模式注册菜单项:

WP-Admin 模式集成在标准 WordPress 管理界面中(保留侧边栏和顶部栏)。菜单项应使用简单别名和回调注册:

add_submenu_page(
    'themes.php',                                      // 父级菜单
    __( 'My Page', 'my-plugin' ),                     // 页面标题
    __( 'My Page', 'my-plugin' ),                     // 菜单标题
    'edit_theme_options',                              // 权限
    'my-admin-page-wp-admin',                          // 菜单别名(简单)
    'my_plugin_my_admin_page_wp_admin_render_page'     // 来自生成的 PHP 的回调(带前缀)
);

注意:回调函数名称会带有你的插件名前缀(来自根目录 package.json 中的 wpPlugin.name)。例如,如果你的插件名是 my-plugin,该函数将是 my_plugin_my_admin_page_wp_admin_render_page。

页面别名是 my-admin-page-wp-admin(你的页面 ID + -wp-admin)。WordPress 会将所有请求路由到该回调,JavaScript 路由器负责处理内部导航。

使用 p 查询参数进行深度链接:

用户和扩展可以使用 p 查询参数直接链接到特定路由:

// 链接到特定路由
$url = admin_url( 'admin.php?page=my-admin-page-wp-admin&p=' . urlencode( '/settings' ) );

页面加载时,JavaScript 启动系统会读取 p 参数,并自动导航到该路由。

为整页模式注册菜单项:

整页模式会使用自定义侧边栏接管整个管理屏幕:

add_menu_page( 'Title', 'Menu', 'capability', 'my-admin-page', 'my_plugin_my_admin_page_render_page', 'icon', 20 );

Init 模块:
Init 模块是在页面初始化期间执行的 JavaScript 包,会在菜单项和路由注册之后、应用渲染之前运行。它们非常适合用于:

  • 为菜单项添加图标(图标无法从 PHP 传递)
  • 注册命令面板条目

创建 Init 模块:

在 packages/my-page-init/package.json 中:

{
    "name": "@my-plugin/my-page-init",
    "wpScriptModuleExports": "./build-module/index.js",
    "dependencies": {
        "@wordpress/boot": "file:../boot",
        "@wordpress/data": "file:../data",
        "@wordpress/icons": "file:../icons"
    }
}

在 packages/my-page-init/src/index.ts 中:

import { home, styles } from '@wordpress/icons';
import { dispatch } from '@wordpress/data';
import { store as bootStore } from '@wordpress/boot';

/**
 * 初始化页面 - 此函数是必需的。
 * 所有 init 模块都必须导出一个 'init' 函数。
 */
export async function init() {
    // 为菜单项添加图标
    dispatch( bootStore ).updateMenuItem( 'home', { icon: home } );
    dispatch( bootStore ).updateMenuItem( 'styles', { icon: styles } );
}

init() 函数是必需的——所有 init 模块都必须导出这个命名函数。Init 模块会作为静态依赖加载,并在启动系统注册菜单项和路由之后、应用渲染之前按顺序执行。

示例:WordPress Core(Gutenberg)

{
    "wpPlugin": {
        "scriptGlobal": "wp",
        "packageNamespace": "wordpress"
    }
}

此配置:

  • 像 @wordpress/data 这样的包会暴露 window.wp.data
  • 像 @wordpress/block-editor 这样的包会暴露 window.wp.blockEditor
  • 所有包都可以将 @wordpress/* 作为外部依赖使用

示例:第三方插件

{
    "wpPlugin": {
        "name": "acme",
        "scriptGlobal": "acme",
        "packageNamespace": "acme"
    }
}

此配置:

  • 像 @acme/editor 这样的包会暴露 window.acme.editor
  • 像 @acme/data 这样的包会暴露 window.acme.data
  • 所有包仍然可以使用 @wordpress/* → window.wp.*
  • 所有包仍然可以使用第三方依赖(react、lodash)→ window.React、window.lodash

行为

  • 带有 wpScript: true 且匹配命名空间的包:打包并暴露到全局
  • 带有 wpScript: true 但不匹配命名空间的包:打包但不暴露到全局
  • 依赖:@wordpress/* 包始终被外部化为 wp.* 全局变量
  • 第三方依赖:React、lodash、jQuery、moment 始终被外部化为它们的标准全局变量
  • 资产文件:始终会生成 .asset.php 文件,用于 WordPress 依赖管理

输出结构

构建工具会在 build/ 目录中生成多个文件,但主要输出是 PHP 注册文件。

请确保在你的插件文件中引入生成的 PHP 文件。

require_once plugin_dir_path( __FILE__ ) . 'build/build.php';

路由(实验性)

路由为 WordPress 管理页面提供基于文件的路由系统。每个路由都必须关联到在 wpPlugin.pages 中定义的页面(见上文)。请在仓库根目录创建 routes/ 目录,并为每个路由创建子目录。

路由结构

routes/
  home/
    package.json    # 路由配置
    stage.tsx       # 主内容组件
    inspector.tsx   # 可选侧边栏组件
    canvas.tsx      # 可选自定义画布组件
    route.tsx       # 可选生命周期钩子(beforeLoad、loader、canvas)

路由配置

在 routes/{route-name}/package.json 中:

{
    "route": {
        "path": "/",
        "page": "my-admin-page"
    }
}

对于应出现在多个页面上的路由:

{
    "route": {
        "path": "/settings",
        "page": [ "my-admin-page", "other-page" ]
    }
}

page 字段可以是:

  • 字符串:路由属于单个页面
  • 数组:路由出现在多个页面上(构建系统会为每个页面注册该路由)

每个页面 ID 都必须匹配根目录 package.json 中 wpPlugin.pages 里定义的页面之一。这会告诉构建系统该路由属于哪个(些)页面。它也可以映射到其他插件已注册的现有页面。

多页面路由适用于在不同管理页面之间共享的功能,例如可从主页面和专用设置页面访问的设置路由。

路由会继承其页面的实验状态:只有当该路由所属的每个页面都在 wpPlugin.pages 中标记为 "experimental": true 时,它才会从 WordPress Core 构建中排除。

组件

stage.tsx – 主内容(必需):

export const stage = () => <div>Content</div>;

inspector.tsx – 侧边栏内容(可选):

export const inspector = () => <div>Inspector</div>;

canvas.tsx – 自定义画布组件(可选):

export const canvas = () => <div>Custom Canvas</div>;

画布是一个全屏区域,通常用于编辑器预览。要使用自定义画布,请从 canvas.tsx 导出它,并在 route.canvas() 中返回 null。

生命周期钩子(route.tsx)

导出一个带有可选钩子的 route 对象。每个钩子都会接收 { params, search }——来自 URL 的路径参数和查询字符串值。

  • beforeLoad — 在导航完成前运行。用于身份验证检查、校验或重定向。抛出 @wordpress/route 中的 redirect() 或 notFound() 以中止导航。
  • loader — 在路由加载时运行。用于预加载数据(例如,来自 @wordpress/data 的 resolveSelect),以便 stage 组件可以从 store 读取,而无需加载状态。可以返回一个对象,其属性会合并到该路由的 loader 数据中。对于必须在 JavaScript 应用加载前可用的数据,请在页面的 PHP 渲染回调中使用 rest_preload_api_request()。
  • canvas — 与 loader 并行运行。根据返回值控制渲染哪个画布:
    • CanvasData({ postType, postId, isPreview?, editLink? })→ 默认的 WordPress 编辑器画布
    • null → 自定义 canvas.tsx 组件(如果已提供)
    • undefined 或省略 → 不显示画布
export const route = {
    beforeLoad: ( { params, search } ) => {
        // 导航前校验、身份验证检查
    },
    loader: ( { params, search } ) => {
        // 数据预加载
    },
    canvas: ( { params, search } ) => {
        // 返回 CanvasData 以使用默认画布(编辑器)
        return {
            postType: params.type,
            postId: params.id,
            isPreview: true,
            editLink: `/types/${ params.type }/edit/${ params.id }`,
        };

        // 返回 null 以使用自定义 canvas.tsx 组件
        // return null;

        // 返回 undefined 以不显示画布
        // return undefined;
    },
};

Routes 构建输出

构建系统会生成:

  • build/routes/{route-name}/content.js – 打包后的 stage/inspector/canvas 组件
  • build/routes/{route-name}/route.js – 打包后的生命周期钩子(如果存在)
  • build/routes/registry.php – 路由注册数据
  • build/routes.php – 路由注册逻辑

Gutenberg 中的 boot 包会自动使用这些路由并使其可用。

Widgets(实验性)

[!注意]
Widgets 仍为实验性功能。“实验性”表示这是一个早期实现,可能发生重大且具有破坏性的变更。

Widgets 提供了一个基于文件的发现系统,用于构建注册为 WordPress 脚本模块的自包含 UI 组件。每个 widget 都位于仓库根目录下 widgets/ 中的独立目录内。

Widget 结构

widgets/
  hello-world/
    widget.json     # 静态发现元数据(必需)
    widget.ts       # 运行时值入口(可选)
    render.tsx      # UI 组件入口(可选)
    render.scss     # 可选样式(从 render.tsx 导入时会内联打包)
    package.json    # 可选 npm 依赖清单

为什么有两个入口?

Widgets 使用双入口模式,这在理念上类似于区块在 block.json 和 edit.js 之间拆分元数据的方式:

关注点所在位置原因
身份(name)两者(必须一致)服务器端需要用它注册;客户端使用它来解析运行时入口
可翻译元数据(title、description、help、keywords)widget.json通过 textdomain 在服务器端翻译,因此宿主无需 JS 运行时即可接收本地化字符串
框架信息(category、presentation)widget.json纯 JSON,宿主无需 JS 运行时即可读取
属性 Schema(类型、选项、标签、relevance)widget.json纯 JSON;标签通过 textdomain 在服务器端翻译;具有相同 id 的 widget.ts 入口可添加 JSON 无法表达的内容
example、需要代码的属性 propswidget.ts运行时值

经验法则:宿主在加载某个 widget 的模块之前需要的任何内容——身份、可翻译文案、框架信息、属性 Schema——都放在 widget.json 中;构建会将其转发到 build/widgets/registry.php,并通过 REST API 暴露。任何需要代码的内容(example、某个属性的 Edit 组件或自定义校验器)则放在 widget.ts 中。

widget.json — 静态发现元数据

{
    "name": "my-plugin/hello-world",
    "title": "Hello World",
    "description": "A simple example widget.",
    "help": {
        "content": "A short note for compact surfaces such as tooltips."
    },
    "category": "demo",
    "textdomain": "my-plugin"
}

字段:

  • name(必需):命名空间标识符(例如 "my-plugin/hello-world")
  • title(可选):人类可读的标题。使用 textdomain 在服务器端翻译。
  • description(可选):简短描述。使用 textdomain 在服务器端翻译。
  • help(可选):用于紧凑界面的上下文帮助说明。它是一个对象,包含 content(可包含 <em>/<strong>)和可选的 links(label、href)。使用 textdomain 在服务器端翻译。
  • keywords(可选):搜索别名。使用 textdomain 在服务器端翻译。
  • attributes(可选):属性 Schema,每个属性一个条目,使用 DataViews Field 中可用 JSON 表达的子集(id、type、label、elements、不含 custom 的 isValid 等),以及可选的 relevance(high、medium、low)。type 可以指定一个已注册的字段类型。label、header、description、placeholder,以及选项的标签和描述,会使用 textdomain 在服务器端翻译。
  • category(可选):用于筛选的分组类别
  • presentation(可选):渲染意图(framed、content-bleed、full-bleed)
  • textdomain(可选):用于翻译 title、description、help、keywords 和属性标签的 Gettext 文本域

widget.ts — 运行时值

可选。导出一个默认对象,其中包含需要代码的内容:example 数据,以及为 widget.json 中具有相同 id 的属性添加 Edit 组件或自定义校验器的属性条目。构建系统会在注册时注入 render_module 句柄,因此作者无需声明它。

import type { WidgetAttributeField } from '@wordpress/widget-primitives';
import { GreetingControl } from './greeting-control';

type HelloWorldAttributes = {
    greeting: string;
    showDate: boolean;
};

const widget = {
    name: 'my-plugin/hello-world',
    attributes: [ { id: 'greeting', Edit: GreetingControl } ] satisfies Partial<
        WidgetAttributeField< HelloWorldAttributes >
    >[],
    example: {
        attributes: {
            greeting: 'World',
            showDate: true,
        },
    },
};

export default widget;

条目会通过 id 与 widget.json Schema 合并:对于共享键,JSON 值优先,模块补充其余内容,而 isValid 会逐条规则合并,因此 JSON 规则和模块 custom 校验器可以共存。如果某个 widget 的属性只是普通数据,则不需要 widget.ts。

render.tsx — UI 组件

render 入口接收与 widget.json 中声明的 Schema 匹配的 attributes。它会在打包时支持 CSS(可直接导入 .scss / .css)。

interface HelloWorldRenderProps {
    attributes: {
        greeting?: string;
        showDate?: boolean;
    };
}

export default function HelloWorld( { attributes }: HelloWorldRenderProps ) {
    return (
        <div>
            Hello, { attributes.greeting ?? 'World' }!
            { attributes.showDate && (
                <time>{ new Date().toLocaleDateString() }</time>
            ) }
        </div>
    );
}

所有非 JSON 入口都是可选的。构建系统会按优先级顺序检查这些扩展名的文件:.tsx、.ts、.jsx、.js、.mjs。

Widgets 构建输出

构建系统会生成:

  • build/widgets/{widget-name}/render.min.js + render.js — 打包后的 UI 组件(ESM)
  • build/widgets/{widget-name}/render.min.asset.php — 渲染模块的资产元数据
  • build/widgets/{widget-name}/widget.min.js + widget.js — 捆绑元数据(ESM)
  • build/widgets/{widget-name}/widget.min.asset.php — 小工具模块的资源元数据
  • build/widgets/registry.php — 小工具注册数据
  • build/widgets.php — 脚本模块注册逻辑
  • PHP 注册

    生成的 widgets.php 通过 wp_register_script_module() 将每个小工具的条目注册为脚本模块。模块句柄遵循以下模式:

    {handlePrefix}/widgets/{widget-dir-name}/render
    {handlePrefix}/widgets/{widget-dir-name}/widget
    

    注册被挂载到 init 动作。SCRIPT_DEBUG 常量控制加载压缩(.min.js)还是未压缩(.js)文件。

    监听模式

    在开发环境(wp-build --watch)中,会监听小工具源文件的变更。当 widgets/{name}/ 内的文件发生变化时,仅重建该小工具。

    为此包做贡献

    这是一个独立的软件包,是 Gutenberg 项目的一部分。该项目以 monorepo 形式组织,由多个自包含的软件包组成,每个软件包都有特定用途。

    此 monorepo 中的软件包发布到 npm,并被 WordPress 以及其他软件项目使用。

    要了解更多关于为此软件包或整个 Gutenberg 做贡献的信息,请阅读项目的主要 贡献者指南。

    许可证

    GPL-2.0-or-later © WordPress 贡献者