@cloudcatch/wp-esbuild 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CloudCatch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,516 @@
1
+ # @cloudcatch/wp-esbuild
2
+
3
+ **Fast esbuild-based builds for WordPress plugins and themes.**
4
+
5
+ wp-esbuild is a build tool for modern WordPress development. It compiles your blocks, JavaScript, and styles into optimized assets your plugin or theme can enqueue in PHP — with sensible defaults and a config file when you need more control.
6
+
7
+ Works with the standard `src/` → `build/` layout out of the box. Supports TypeScript, script modules, SCSS, custom directory structures, and multiple build pipelines.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install --save-dev @cloudcatch/wp-esbuild
13
+ ```
14
+
15
+ Requires Node.js >= 18.
16
+
17
+ ```json
18
+ {
19
+ "scripts": {
20
+ "build": "NODE_ENV=production wp-esbuild",
21
+ "start": "wp-esbuild --watch"
22
+ }
23
+ }
24
+ ```
25
+
26
+ Use `NODE_ENV=production` for minified output; omit it during development for source maps.
27
+
28
+ ## Quick start
29
+
30
+ ### Project layout
31
+
32
+ With no config file, wp-esbuild uses this structure:
33
+
34
+ ```
35
+ my-plugin/
36
+ ├── wp-esbuild.config.mjs # optional
37
+ ├── package.json
38
+ ├── src/
39
+ │ ├── blocks/
40
+ │ │ └── my-block/
41
+ │ │ ├── block.json
42
+ │ │ ├── index.js
43
+ │ │ ├── style.scss → build/blocks/my-block/style-index.css
44
+ │ │ ├── editor.scss → build/blocks/my-block/index.css
45
+ │ │ ├── view.js → build/blocks/my-block/view.js
46
+ │ │ └── render.php
47
+ │ ├── js/
48
+ │ │ ├── admin.js
49
+ │ │ └── modules/
50
+ │ │ └── my-module.js
51
+ │ └── scss/
52
+ │ └── main.scss
53
+ └── build/
54
+ ```
55
+
56
+ Run `npm run build`. Each JS bundle gets a sibling `*.asset.php` with `dependencies` and `version`.
57
+
58
+ ### Minimal config
59
+
60
+ ```js
61
+ // wp-esbuild.config.mjs
62
+ import { defineConfig } from '@cloudcatch/wp-esbuild/config';
63
+
64
+ export default defineConfig( {
65
+ minify: process.env.NODE_ENV === 'production',
66
+ sourcemap: process.env.NODE_ENV !== 'production',
67
+ } );
68
+ ```
69
+
70
+ ## CLI
71
+
72
+ ```bash
73
+ npx wp-esbuild # build
74
+ npx wp-esbuild --watch # watch and rebuild
75
+ npx wp-esbuild --root ./path/to/plugin # build a specific project
76
+ npx wp-esbuild --blocks-manifest # regenerate blocks-manifest.php only
77
+ ```
78
+
79
+ | Flag | Alias | Description |
80
+ | --- | --- | --- |
81
+ | `--root <path>` | `-r` | Project root (default: current directory). |
82
+ | `--watch` | `-w` | Watch for changes and rebuild. |
83
+ | `--blocks-manifest` | | Generate the blocks manifest only. |
84
+
85
+ ## Configuration
86
+
87
+ Add `wp-esbuild.config.mjs` to your project root. Export a config object or a function:
88
+
89
+ ```js
90
+ export default ( { env } ) => ( {
91
+ minify: env.NODE_ENV === 'production',
92
+ } );
93
+ ```
94
+
95
+ `defineConfig` from `@cloudcatch/wp-esbuild/config` is optional; it warns about unknown keys and invalid entry definitions.
96
+
97
+ ### Two ways to define pipelines
98
+
99
+ **Shorthand keys** — `blocks`, `js`, `modules`, `scss`, and `copy`. Each accepts an object or an array of objects. These are merged with defaults and run automatically when no `entries` array is set.
100
+
101
+ **`entries` array** — explicit, named pipelines. When `entries` is provided, shorthand keys are ignored.
102
+
103
+ ### Global options
104
+
105
+ | Key | Default | Description |
106
+ | --- | --- | --- |
107
+ | `srcDir` | `'src'` | Default source root for shorthand paths. |
108
+ | `outDir` | `'build'` | Default output root for shorthand paths. |
109
+ | `entries` | — | Unified pipeline list (see below). |
110
+ | `blocks` | see defaults | Block compilation pipeline. |
111
+ | `js` | `{ src: 'src/js', out: 'build/js' }` | IIFE script bundles. |
112
+ | `modules` | `{ src: 'src/js/modules', out: 'build/js/modules' }` | ESM script modules. |
113
+ | `scss` | `{ src: 'src/scss', out: 'build/css' }` | Standalone stylesheets. |
114
+ | `copy` | `[]` | Static file copy tasks. |
115
+ | `minify` | `NODE_ENV === 'production'` | Minify JS and CSS output. |
116
+ | `sourcemap` | `NODE_ENV !== 'production'` | Emit source maps. |
117
+ | `esbuild` | `{}` | Global esbuild options (`target`, `define`, `alias`, `plugins`, `loader`). |
118
+ | `wordpressExternals` | `{}` | Dependency extraction settings. |
119
+ | `postcss` | `true` | PostCSS processing for CSS (see [PostCSS](#postcss)). |
120
+ | `rtl` | `false` | Generate `-rtl.css` files for SCSS output. |
121
+ | `plugins` | `[]` | Custom build hooks (see [Build plugins](#build-plugins)). |
122
+ | `blocksManifest` | enabled | `blocks-manifest.php` generation (see [Blocks manifest](#blocks-manifest)). |
123
+
124
+ #### `blocksManifest`
125
+
126
+ | Key | Default | Description |
127
+ | --- | --- | --- |
128
+ | `enabled` | `true` | Generate manifest after block builds. |
129
+ | `input` | `{outDir}/blocks` | Directory containing built `block.json` files. |
130
+ | `output` | `{outDir}/blocks/blocks-manifest.php` | Output file path. |
131
+
132
+ ### Custom source and output roots
133
+
134
+ Set `srcDir` and `outDir` to avoid repeating paths:
135
+
136
+ ```js
137
+ export default defineConfig( {
138
+ srcDir: 'client',
139
+ outDir: 'public',
140
+ // shorthand defaults become client/blocks → public/blocks, etc.
141
+ } );
142
+ ```
143
+
144
+ Explicit `src` / `out` on any pipeline override these defaults.
145
+
146
+ ### Shorthand pipelines
147
+
148
+ Default behavior when using shorthand keys:
149
+
150
+ | Key | Source | Output | Notes |
151
+ | --- | --- | --- | --- |
152
+ | `blocks` | `{srcDir}/blocks` | `{outDir}/blocks` | Discovers `*/block.json` |
153
+ | `js` | `{srcDir}/js` | `{outDir}/js` | IIFE, all `*.{js,jsx,mjs,ts,tsx}` |
154
+ | `modules` | `{srcDir}/js/modules` | `{outDir}/js/modules` | ESM, recursive glob |
155
+ | `scss` | `{srcDir}/scss` | `{outDir}/css` | Ignores partials (`**/_*`) |
156
+ | `copy` | — | — | `{ from, to }` paths |
157
+
158
+ Override a single admin bundle and copy static assets:
159
+
160
+ ```js
161
+ export default defineConfig( {
162
+ js: {
163
+ src: 'src/js',
164
+ out: 'build/js',
165
+ glob: 'admin.js',
166
+ },
167
+ copy: [
168
+ { from: 'src/assets', to: 'build/assets' },
169
+ { from: 'src/icons/**/*.svg', to: 'build/icons', flatten: true },
170
+ ],
171
+ } );
172
+ ```
173
+
174
+ Multiple JS output directories:
175
+
176
+ ```js
177
+ export default defineConfig( {
178
+ js: [
179
+ { src: 'src/js', out: 'build/js', glob: '*.{js,jsx}' },
180
+ { src: 'src/admin', out: 'build/admin', glob: '**/*.{js,ts,tsx}' },
181
+ ],
182
+ } );
183
+ ```
184
+
185
+ ### Entries API
186
+
187
+ Use `entries` for full control over directory layout, nested blocks, or mixed pipeline types:
188
+
189
+ ```js
190
+ export default defineConfig( {
191
+ entries: [
192
+ {
193
+ name: 'blocks',
194
+ type: 'blocks',
195
+ src: 'src/blocks',
196
+ out: 'build/blocks',
197
+ discover: '*/block.json',
198
+ copy: [ 'block.json', 'render.php' ],
199
+ },
200
+ {
201
+ name: 'admin',
202
+ type: 'script',
203
+ src: 'src/js',
204
+ out: 'build/js',
205
+ glob: '**/*.{js,ts,tsx}',
206
+ format: 'iife',
207
+ },
208
+ {
209
+ name: 'modules',
210
+ type: 'script',
211
+ src: 'src/js/modules',
212
+ out: 'build/js/modules',
213
+ glob: '**/*.{js,ts}',
214
+ format: 'esm',
215
+ },
216
+ {
217
+ name: 'styles',
218
+ type: 'scss',
219
+ src: 'src/scss',
220
+ out: 'build/css',
221
+ glob: '**/*.scss',
222
+ ignore: [ '**/_*' ],
223
+ },
224
+ {
225
+ name: 'assets',
226
+ type: 'copy',
227
+ from: 'src/assets',
228
+ to: 'build/assets',
229
+ },
230
+ ],
231
+ } );
232
+ ```
233
+
234
+ Shared entry fields: `name`, `type`, `enabled` (set `false` to skip), `esbuild` (per-entry overrides).
235
+
236
+ ## Entry types
237
+
238
+ ### Blocks
239
+
240
+ Discovers block directories via a glob, then compiles each block:
241
+
242
+ | Source | Output |
243
+ | --- | --- |
244
+ | `index.js` | `index.js` + `index.asset.php` |
245
+ | `view.js` | `view.js` + `view.asset.php` (ESM when `viewScriptModule` is set in `block.json`) |
246
+ | `style.scss` | `style-index.css` |
247
+ | `editor.scss` | `index.css` |
248
+ | `block.json`, `render.php`, … | Copied as-is |
249
+
250
+ The block entry file must be named `index.js` (it can import `.ts` / `.tsx` files). Output slug is the block folder name. Nested discovery (`**/block.json`) flattens to the directory basename.
251
+
252
+ | Option | Default | Description |
253
+ | --- | --- | --- |
254
+ | `discover` | `'*/block.json'` | Glob under `src` for block.json files. |
255
+ | `copy` | `[ 'block.json', 'render.php' ]` | Files to copy from each block directory. |
256
+ | `rtl` | global setting | Generate RTL CSS for block styles. |
257
+
258
+ Reference compiled assets in `block.json` with `file:./index.js`, `file:./style-index.css`, etc.
259
+
260
+ ### Scripts
261
+
262
+ Compiles files matching `glob` in `src` to `.js` in `out`.
263
+
264
+ | Option | Default | Description |
265
+ | --- | --- | --- |
266
+ | `glob` | `*.{js,jsx,mjs,ts,tsx}` | Entry file pattern. |
267
+ | `format` | `'iife'` | `'iife'` or `'esm'`. |
268
+ | `wordpressExternals` | `true` (IIFE) | Externalize `@wordpress/*` and emit `.asset.php`. |
269
+ | `assetPhp` | `true` | Write dependency file beside each bundle. |
270
+ | `extractCss` | `false` | Compile imported styles into the bundle instead of stripping them. |
271
+
272
+ Recursive globs preserve directory structure in the output.
273
+
274
+ ### SCSS
275
+
276
+ Compiles standalone stylesheets (block styles are handled by the blocks pipeline).
277
+
278
+ | Option | Default | Description |
279
+ | --- | --- | --- |
280
+ | `glob` | `*.{scss,sass}` | Entry file pattern. |
281
+ | `ignore` | `[ '**/_*' ]` | Skip partials. |
282
+ | `outName` | `'preserve'` or `'flat'` | Output naming (see below). |
283
+ | `assetPhp` | `false` | Write `{name}.asset.php` with a content hash version. |
284
+ | `assetDependencies` | `[]` | Dependencies listed in CSS `.asset.php`. |
285
+ | `rtl` | global setting | Write `{name}-rtl.css` alongside each file. |
286
+
287
+ **`outName` values:**
288
+
289
+ | Value | Example input | Output |
290
+ | --- | --- | --- |
291
+ | `'preserve'` | `blocks/core/button.scss` | `blocks/core/button.css` |
292
+ | `'flat'` | `blocks/core/button.scss` | `button.css` |
293
+ | `{ join: '-', tail: 2 }` | `blocks/core/button.scss` | `core-button.css` |
294
+ | `{ join: '-' }` | `blocks/core/button.scss` | `blocks-core-button.css` |
295
+
296
+ Use `tail` to take the last N path segments (filename included) and `join` to flatten them into a single output filename. Handy when PHP expects flat CSS files derived from nested source paths.
297
+
298
+ ### Copy
299
+
300
+ | Option | Description |
301
+ | --- | --- |
302
+ | `from` | Source path or glob, relative to project root. |
303
+ | `to` | Destination path, relative to project root. |
304
+ | `flatten` | When `true`, glob matches are copied flat into `to`. |
305
+
306
+ ## Blocks manifest
307
+
308
+ When a blocks pipeline runs and `blocksManifest.enabled` is `true`, wp-esbuild writes a PHP file mapping block slugs to their `block.json` contents.
309
+
310
+ Register blocks in WordPress 6.7+:
311
+
312
+ ```php
313
+ $blocks_dir = plugin_dir_path( __FILE__ ) . 'build/blocks';
314
+
315
+ wp_register_block_metadata_collection(
316
+ $blocks_dir,
317
+ $blocks_dir . '/blocks-manifest.php'
318
+ );
319
+
320
+ $manifest = require $blocks_dir . '/blocks-manifest.php';
321
+ foreach ( array_keys( $manifest ) as $slug ) {
322
+ register_block_type( $blocks_dir . '/' . $slug );
323
+ }
324
+ ```
325
+
326
+ Custom manifest paths:
327
+
328
+ ```js
329
+ blocksManifest: {
330
+ input: 'public/features',
331
+ output: 'public/features/blocks-manifest.php',
332
+ },
333
+ ```
334
+
335
+ ## WordPress externals
336
+
337
+ IIFE bundles externalize `@wordpress/*` imports to `window.wp.*` globals and list script handles in `*.asset.php`. Common npm packages map to WordPress globals:
338
+
339
+ | Import | Handle |
340
+ | --- | --- |
341
+ | `react` | `react` |
342
+ | `react-dom` | `react-dom` |
343
+ | `lodash` / `lodash-es` | `lodash` |
344
+ | `jquery` | `jquery` |
345
+ | `moment` | `moment` |
346
+
347
+ ### Bundling packages
348
+
349
+ Some `@wordpress/*` packages have no script handle and are bundled by default (`@wordpress/icons`, `@wordpress/dataviews`, and others). Add more to the bundle list:
350
+
351
+ ```js
352
+ wordpressExternals: {
353
+ bundle: [ '@wordpress/icons', '@wordpress/dataviews' ],
354
+ },
355
+ ```
356
+
357
+ Force a bundled package back to external, or map custom vendors:
358
+
359
+ ```js
360
+ wordpressExternals: {
361
+ external: [ '@wordpress/icons' ],
362
+ vendors: {
363
+ 'my-lib': { global: 'MyLib', handle: 'my-lib' },
364
+ },
365
+ },
366
+ ```
367
+
368
+ ### Script modules
369
+
370
+ ESM bundles (`format: 'esm'`) emit module-compatible `.asset.php` files for `wp_register_script_module()`:
371
+
372
+ ```php
373
+ return array(
374
+ 'dependencies' => array( '@wordpress/interactivity' ),
375
+ 'version' => '…',
376
+ 'type' => 'module',
377
+ );
378
+ ```
379
+
380
+ Set `"viewScriptModule": "file:./view.js"` in `block.json` to build block view scripts as modules.
381
+
382
+ ## PostCSS
383
+
384
+ | Value | Behavior |
385
+ | --- | --- |
386
+ | `true` | Load `postcss.config.mjs` / `.js` / `.cjs`, or use Autoprefixer. |
387
+ | `false` | Skip PostCSS. |
388
+ | `[ plugins ]` | Use a custom plugin array. |
389
+
390
+ ### RTL
391
+
392
+ ```js
393
+ export default defineConfig( { rtl: true } );
394
+ ```
395
+
396
+ Generates `main-rtl.css` next to each `main.css`. Enqueue when `is_rtl()`:
397
+
398
+ ```php
399
+ wp_enqueue_style( 'my-admin', $url . 'main.css', [], $ver );
400
+ if ( is_rtl() ) {
401
+ wp_enqueue_style( 'my-admin-rtl', $url . 'main-rtl.css', [ 'my-admin' ], $ver );
402
+ }
403
+ ```
404
+
405
+ ## TypeScript and JSX
406
+
407
+ `.ts`, `.tsx`, `.js`, and `.jsx` are supported out of the box. Block registration uses `index.js` as the entry point; admin and module pipelines can target `**/*.{ts,tsx}` directly.
408
+
409
+ ## esbuild options
410
+
411
+ ```js
412
+ export default defineConfig( {
413
+ esbuild: {
414
+ target: 'es2020',
415
+ define: { 'process.env.NODE_ENV': '"production"' },
416
+ alias: { '@': './src' },
417
+ loader: { '.svg': 'text' },
418
+ },
419
+ } );
420
+ ```
421
+
422
+ Defaults: `bundle: true`, `platform: 'browser'`, `target: 'es2018'`, automatic JSX, and file loaders for images and fonts.
423
+
424
+ Per-entry `esbuild` options merge on top of global settings.
425
+
426
+ ## Enqueueing in PHP
427
+
428
+ **Script bundle:**
429
+
430
+ ```php
431
+ $asset = require __DIR__ . '/build/js/admin.asset.php';
432
+
433
+ wp_enqueue_script(
434
+ 'my-plugin-admin',
435
+ plugins_url( 'build/js/admin.js', __FILE__ ),
436
+ $asset['dependencies'],
437
+ $asset['version'],
438
+ true
439
+ );
440
+ ```
441
+
442
+ **Script module:**
443
+
444
+ ```php
445
+ $asset = require __DIR__ . '/build/js/modules/my-module.asset.php';
446
+
447
+ wp_register_script_module(
448
+ 'my-plugin/my-module',
449
+ plugins_url( 'build/js/modules/my-module.js', __FILE__ ),
450
+ $asset['dependencies'],
451
+ $asset['version']
452
+ );
453
+ ```
454
+
455
+ ## Watch mode
456
+
457
+ `wp-esbuild --watch` debounces file changes and rebuilds affected pipelines. The blocks manifest regenerates when block output changes.
458
+
459
+ ## Build plugins
460
+
461
+ Run custom steps after each build:
462
+
463
+ ```js
464
+ export default defineConfig( {
465
+ plugins: [
466
+ {
467
+ name: 'my-plugin',
468
+ watch: [ 'config/schema.json' ],
469
+ async build( { projectRoot, config, entries } ) {
470
+ // custom build logic
471
+ },
472
+ },
473
+ ],
474
+ } );
475
+ ```
476
+
477
+ ## Programmatic API
478
+
479
+ ```js
480
+ import { build, defineConfig } from '@cloudcatch/wp-esbuild';
481
+ import { buildBlocksManifest } from '@cloudcatch/wp-esbuild/blocks-manifest';
482
+
483
+ await build( process.cwd(), { watch: false } );
484
+
485
+ await buildBlocksManifest( {
486
+ projectRoot: process.cwd(),
487
+ inputDir: 'build/blocks',
488
+ outputFile: 'build/blocks/blocks-manifest.php',
489
+ } );
490
+ ```
491
+
492
+ | Import | Exports |
493
+ | --- | --- |
494
+ | `@cloudcatch/wp-esbuild` | `build`, `defineConfig`, `normalizeConfig` |
495
+ | `@cloudcatch/wp-esbuild/config` | `defineConfig` |
496
+ | `@cloudcatch/wp-esbuild/blocks-manifest` | `buildBlocksManifest` |
497
+ | `@cloudcatch/wp-esbuild/wordpress-externals` | Externals esbuild plugins |
498
+
499
+ ## Migrating from `@wordpress/scripts`
500
+
501
+ 1. Replace `wp-scripts build` with `wp-esbuild` in your scripts.
502
+ 2. Add `wp-esbuild.config.mjs` if you use custom paths or multiple bundles.
503
+ 3. Keep block sources under `src/blocks/*/block.json` (or set `discover`).
504
+ 4. Enqueue assets using the generated `*.asset.php` files — same format as `@wordpress/scripts`.
505
+
506
+ | `@wordpress/scripts` | wp-esbuild |
507
+ | --- | --- |
508
+ | Default entries | `blocks`, `js`, `modules`, `scss` shorthand keys |
509
+ | `build-blocks-manifest` | `blocksManifest.enabled` |
510
+ | Dependency extraction | `wordpressExternals` |
511
+ | PostCSS | `postcss: true` + optional config file |
512
+ | RTL | `rtl: true` |
513
+
514
+ ## License
515
+
516
+ MIT
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for @cloudcatch/wp-esbuild.
4
+ */
5
+ import path from 'path';
6
+ import { parseArgs } from 'util';
7
+ import { build } from '../lib/build.mjs';
8
+ import { buildBlocksManifest } from '../lib/build-blocks-manifest.mjs';
9
+ import { loadProjectConfig } from '../lib/load-config.mjs';
10
+
11
+ const { values } = parseArgs( {
12
+ options: {
13
+ root: { type: 'string', short: 'r' },
14
+ watch: { type: 'boolean', short: 'w', default: false },
15
+ 'blocks-manifest': { type: 'boolean', default: false },
16
+ },
17
+ allowPositionals: true,
18
+ } );
19
+
20
+ const projectRoot = path.resolve( values.root || process.cwd() );
21
+
22
+ try {
23
+ if ( values[ 'blocks-manifest' ] ) {
24
+ const config = await loadProjectConfig( projectRoot );
25
+ await buildBlocksManifest( {
26
+ projectRoot,
27
+ inputDir: config.blocksManifest.input,
28
+ outputFile: config.blocksManifest.output,
29
+ } );
30
+ } else {
31
+ await build( projectRoot, { watch: values.watch } );
32
+ }
33
+ } catch ( error ) {
34
+ console.error( error );
35
+ process.exit( 1 );
36
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Generate blocks-manifest.php from block.json files in a build directory.
3
+ *
4
+ * Mirrors @wordpress/scripts/scripts/build-blocks-manifest.js
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import glob from 'fast-glob';
9
+ import { exportToPhp } from './php-export.mjs';
10
+
11
+ /**
12
+ * @param {object} options
13
+ * @param {string} options.projectRoot
14
+ * @param {string} options.inputDir Absolute path to scan for block.json files.
15
+ * @param {string} options.outputFile Absolute path for blocks-manifest.php.
16
+ * @return {Promise<string>} Path to the generated manifest file.
17
+ */
18
+ export async function buildBlocksManifest( {
19
+ projectRoot,
20
+ inputDir,
21
+ outputFile,
22
+ } ) {
23
+ const resolvedInputDir = path.resolve( projectRoot, inputDir );
24
+
25
+ if ( ! fs.existsSync( resolvedInputDir ) ) {
26
+ throw new Error(
27
+ `Blocks manifest input directory does not exist: ${ resolvedInputDir }`
28
+ );
29
+ }
30
+
31
+ const blockJsonFiles = await glob( './**/block.json', {
32
+ cwd: resolvedInputDir,
33
+ absolute: true,
34
+ } );
35
+
36
+ /** @type {Record<string, object>} */
37
+ const blocks = {};
38
+
39
+ for ( const file of blockJsonFiles ) {
40
+ const blockJson = JSON.parse( fs.readFileSync( file, 'utf8' ) );
41
+ const directoryName = path.basename( path.dirname( file ) );
42
+ blocks[ directoryName ] = blockJson;
43
+ }
44
+
45
+ if ( Object.keys( blocks ).length === 0 ) {
46
+ throw new Error(
47
+ `No block.json files found in blocks manifest input: ${ resolvedInputDir }`
48
+ );
49
+ }
50
+
51
+ const phpContent = `<?php
52
+ // This file is generated. Do not modify it manually.
53
+ return ${ exportToPhp( blocks, '\t' ) };
54
+ `;
55
+
56
+ const resolvedOutputFile = path.resolve( projectRoot, outputFile );
57
+ fs.mkdirSync( path.dirname( resolvedOutputFile ), { recursive: true } );
58
+ fs.writeFileSync( resolvedOutputFile, phpContent );
59
+
60
+ console.log( `Block metadata PHP file generated at: ${ resolvedOutputFile }` );
61
+
62
+ return resolvedOutputFile;
63
+ }