From 8cc543b4538f3d74eb4eb396021ef0914371910a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Apr 2026 16:59:09 +0100 Subject: [PATCH 1/8] add wpPlugin.packageSources support via pnpm patch patches @wordpress/build@0.11.0 to add packageSources config for cross-directory package discovery. mirrors upstream Gutenberg PR https://github.com/WordPress/gutenberg/pull/77226 1:1. flags @automattic/number-formatters as a script-module exporter via wpScriptModuleExports so consumers can externalize it. --- .pnpm-patches/@wordpress__build@0.11.0.patch | 620 ++++++++++++++++++ pnpm-lock.yaml | 81 ++- pnpm-workspace.yaml | 4 + .../changelog/update-wp-build-package-sources | 5 + .../number-formatters/package.json | 1 + 5 files changed, 705 insertions(+), 6 deletions(-) create mode 100644 .pnpm-patches/@wordpress__build@0.11.0.patch create mode 100644 projects/js-packages/number-formatters/changelog/update-wp-build-package-sources diff --git a/.pnpm-patches/@wordpress__build@0.11.0.patch b/.pnpm-patches/@wordpress__build@0.11.0.patch new file mode 100644 index 000000000000..aa7deef39aa7 --- /dev/null +++ b/.pnpm-patches/@wordpress__build@0.11.0.patch @@ -0,0 +1,620 @@ +Bridge to upstream Gutenberg PR https://github.com/WordPress/gutenberg/pull/77226. +Adds `wpPlugin.packageSources` to @wordpress/build for cross-directory package discovery. + +When the upstream PR merges and Jetpack bumps @wordpress/build, +delete this file and remove its entry from pnpm-workspace.yaml. + +To regenerate after upstream changes (requires Gutenberg checkout with the feature branch): + + PATCH=.pnpm-patches/@wordpress__build@0.11.0.patch + { + sed '/^diff --git/,$d' "$PATCH" + git -C ~/lab/gutenberg diff --full-index \ + @wordpress/build@0.11.0..add/wp-build-sources-config -- \ + packages/wp-build/lib/build.mjs \ + packages/wp-build/lib/package-utils.mjs \ + packages/wp-build/lib/wordpress-externals-plugin.mjs \ + | sed -e 's|a/packages/wp-build/|a/|g' -e 's|b/packages/wp-build/|b/|g' + } > "$PATCH.new" && mv "$PATCH.new" "$PATCH" + +diff --git a/lib/build.mjs b/lib/build.mjs +index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420383a0d96 100755 +--- a/lib/build.mjs ++++ b/lib/build.mjs +@@ -4,6 +4,7 @@ + * External dependencies + */ + import { readFile, writeFile, copyFile, mkdir, unlink } from 'fs/promises'; ++import { existsSync } from 'fs'; + import path from 'path'; + import { createHash } from 'node:crypto'; + import { parseArgs } from 'node:util'; +@@ -90,29 +91,171 @@ const TEST_FILE_PATTERNS = [ + ]; + + /** +- * Get all package names from the packages directory. ++ * A discovered package in the registry. + * +- * @return {string[]} Array of package names. ++ * @typedef {Object} PackageEntry ++ * @property {string} dir Absolute path to the package directory. ++ * @property {import('./package-utils.mjs').PackageJson} packageJson Parsed package.json contents. ++ * @property {boolean} [externalSource] True when the entry comes from a named ++ * package source (e.g. `@scope/name`). ++ * These packages preserve their own npm ++ * identity for script-module IDs instead ++ * of being scoped under `packageNamespace`. + */ +-function getAllPackages() { +- return glob +- .sync( normalizePath( path.join( PACKAGES_DIR, '*', 'package.json' ) ) ) +- .map( ( packageJsonPath ) => +- path.basename( path.dirname( packageJsonPath ) ) +- ); +-} + +-const PACKAGES = getAllPackages(); + const ROOT_PACKAGE_JSON = getPackageInfoFromFile( + path.join( ROOT_DIR, 'package.json' ) + ); + const WP_PLUGIN_CONFIG = ROOT_PACKAGE_JSON.wpPlugin || {}; ++ ++/** ++ * Check whether a sources entry is a npm package name rather than a ++ * relative/absolute directory path. ++ * ++ * Package names follow the npm naming rules: ++ * - Scoped: `@scope/name` ++ * - Bare: `my-package` (starts with a letter or digit, no path separators) ++ * ++ * Directory paths start with `.`, `/`, or a drive letter on Windows (`C:\`). ++ * ++ * @param {string} source A single entry from `wpPlugin.packageSources`. ++ * @return {boolean} True when the entry looks like a package name. ++ */ ++function isPackageName( source ) { ++ if ( source.startsWith( '@' ) ) { ++ return true; ++ } ++ // Relative or absolute paths. ++ if ( source.startsWith( '.' ) || path.isAbsolute( source ) ) { ++ return false; ++ } ++ // Bare package name (no slashes → not a path). ++ return ! source.includes( '/' ); ++} ++ ++/** ++ * Resolve a npm package name to its directory and parsed package.json. ++ * Uses Node's module resolution from the project root context so that ++ * workspace symlinks (pnpm, yarn, npm) are followed automatically. ++ * ++ * @param {string} npmName Full package name (e.g. `@acme/shared-ui`). ++ * @return {{ dir: string, packageJson: import('./package-utils.mjs').PackageJson }|null} ++ * Resolved entry or null when the package is not resolvable. ++ */ ++function resolveNamedSource( npmName ) { ++ // Read directly from node_modules instead of require.resolve() to ++ // avoid ERR_PACKAGE_PATH_NOT_EXPORTED when the package's `exports` ++ // field does not include `./package.json`. ++ const pkgJsonPath = path.join( ++ ROOT_DIR, ++ 'node_modules', ++ npmName, ++ 'package.json' ++ ); ++ ++ if ( ! existsSync( pkgJsonPath ) ) { ++ console.warn( ++ `⚠️ Source "${ npmName }" could not be resolved. ` + ++ 'Make sure it is listed in dependencies and installed.' ++ ); ++ return null; ++ } ++ ++ return { ++ dir: path.dirname( pkgJsonPath ), ++ packageJson: getPackageInfoFromFile( pkgJsonPath ), ++ }; ++} ++ ++/** ++ * Directories to scan for packages. Always starts with `./packages/`. ++ * Additional directory-type entries from `wpPlugin.packageSources` are ++ * appended. Package-name entries are handled separately in ++ * `getAllPackages()`. ++ * ++ * @type {string[]} ++ */ ++const PACKAGE_SOURCES = WP_PLUGIN_CONFIG.packageSources || []; ++const PACKAGE_DIRS = [ ++ PACKAGES_DIR, ++ ...PACKAGE_SOURCES.filter( ( s ) => ! isPackageName( s ) ).map( ( s ) => ++ path.resolve( ROOT_DIR, s ) ++ ), ++]; ++const NAMED_SOURCES = PACKAGE_SOURCES.filter( isPackageName ); ++ ++/** ++ * Get all packages by scanning every directory in PACKAGE_DIRS and ++ * resolving every named source in NAMED_SOURCES. ++ * ++ * Local packages (`./packages/`) are scanned first, so they take priority. ++ * Named sources are resolved last — they preserve their npm identity ++ * (e.g. `@acme/shared-ui` stays `@acme/shared-ui` in script-module ++ * IDs instead of being rewritten to `@/shared-ui`). ++ * ++ * @return {Map} Map of package names to their entry data. ++ */ ++function getAllPackages() { ++ const registry = new Map(); ++ ++ // 1. Directory-based discovery (local packages first, then source dirs). ++ for ( const dir of PACKAGE_DIRS ) { ++ const pkgJsonPaths = glob.sync( ++ normalizePath( path.join( dir, '*', 'package.json' ) ) ++ ); ++ ++ for ( const pkgJsonPath of pkgJsonPaths ) { ++ const name = path.basename( path.dirname( pkgJsonPath ) ); ++ // First match wins — local packages take priority over ++ // sources-discovered packages. ++ if ( ! registry.has( name ) ) { ++ registry.set( name, { ++ dir: path.dirname( pkgJsonPath ), ++ packageJson: getPackageInfoFromFile( pkgJsonPath ), ++ } ); ++ } ++ } ++ } ++ ++ // 2. Named sources — resolve via require.resolve() and preserve ++ // the full npm name as the registry key. ++ for ( const npmName of NAMED_SOURCES ) { ++ if ( registry.has( npmName ) ) { ++ continue; ++ } ++ ++ const entry = resolveNamedSource( npmName ); ++ if ( entry ) { ++ if ( ! entry.packageJson.wpScriptModuleExports ) { ++ console.warn( ++ `⚠️ Source "${ npmName }" does not declare wpScriptModuleExports. ` + ++ 'Imports will be bundled inline instead of externalized.' ++ ); ++ } ++ ++ registry.set( npmName, { ++ ...entry, ++ externalSource: true, ++ } ); ++ } ++ } ++ ++ return registry; ++} ++ ++const PACKAGES = getAllPackages(); + const SCRIPT_GLOBAL = WP_PLUGIN_CONFIG.scriptGlobal; + const PACKAGE_NAMESPACE = WP_PLUGIN_CONFIG.packageNamespace; + const HANDLE_PREFIX = WP_PLUGIN_CONFIG.handlePrefix || PACKAGE_NAMESPACE; +-const EXTERNAL_NAMESPACES = WP_PLUGIN_CONFIG.externalNamespaces || {}; + const PAGES = WP_PLUGIN_CONFIG.pages || []; + ++const EXTERNAL_NAMESPACES = WP_PLUGIN_CONFIG.externalNamespaces || {}; ++ ++// Individual packages from named sources to externalize. Unlike ++// EXTERNAL_NAMESPACES (which externalizes an entire `@scope/*`), this ++// targets only the exact packages listed in `packageSources`. ++const EXTERNAL_PACKAGES = new Set( NAMED_SOURCES ); ++ + /** + * Interprets a configuration value as a boolean, where `"true"` and `"1"` + * are considered true while all other values are false. +@@ -153,7 +296,8 @@ const wordpressExternalsPlugin = createWordpressExternalsPlugin( + PACKAGE_NAMESPACE, + SCRIPT_GLOBAL, + EXTERNAL_NAMESPACES, +- HANDLE_PREFIX ++ HANDLE_PREFIX, ++ EXTERNAL_PACKAGES + ); + + /** +@@ -480,7 +624,6 @@ function resolveEntryPoint( packageDir, packageJson ) { + */ + async function bundlePackage( packageName, options = {} ) { + const { +- sourceDir = PACKAGES_DIR, + handlePrefix = HANDLE_PREFIX, + scriptGlobal = SCRIPT_GLOBAL, + packageNamespace = PACKAGE_NAMESPACE, +@@ -489,10 +632,10 @@ async function bundlePackage( packageName, options = {} ) { + const builtModules = []; + const builtScripts = []; + const builtStyles = []; +- const packageDir = path.join( sourceDir, packageName ); +- const packageJson = getPackageInfoFromFile( +- path.join( sourceDir, packageName, 'package.json' ) +- ); ++ const packageEntry = PACKAGES.get( packageName ); ++ const packageDir = packageEntry.dir; ++ const packageJson = packageEntry.packageJson; ++ const isExternalSource = !! packageEntry.externalSource; + + const builds = []; + +@@ -653,10 +796,21 @@ async function bundlePackage( packageName, options = {} ) { + ); + } + +- const scriptModuleId = +- exportName === '.' +- ? `@${ packageNamespace }/${ packageName }` +- : `@${ packageNamespace }/${ packageName }/${ fileName }`; ++ // External sources preserve their npm identity as the ++ // script-module ID (e.g. `@acme/shared-ui`). Local ++ // packages are scoped under the plugin's namespace. ++ let scriptModuleId; ++ if ( isExternalSource ) { ++ scriptModuleId = ++ exportName === '.' ++ ? packageName ++ : `${ packageName }/${ fileName }`; ++ } else { ++ scriptModuleId = ++ exportName === '.' ++ ? `@${ packageNamespace }/${ packageName }` ++ : `@${ packageNamespace }/${ packageName }/${ fileName }`; ++ } + + builtModules.push( { + id: scriptModuleId, +@@ -862,7 +1016,7 @@ async function inferStyleDependencies( scriptDependencies, packageName ) { + + const styleDeps = []; + // Get the resolve directory for context-aware package resolution +- const resolveDir = path.join( PACKAGES_DIR, packageName ); ++ const resolveDir = PACKAGES.get( packageName ).dir; + + for ( const scriptHandle of scriptDependencies ) { + // Skip non-package dependencies (like 'react', 'lodash', etc.) +@@ -1215,34 +1369,21 @@ async function generatePagesPhp( pageData, replacements ) { + */ + async function transpilePackage( packageName ) { + const startTime = Date.now(); +- const packageDir = path.join( PACKAGES_DIR, packageName ); +- const packageJson = getPackageInfoFromFile( +- path.join( PACKAGES_DIR, packageName, 'package.json' ) +- ); +- +- if ( ! packageJson ) { +- throw new Error( +- `Could not find package.json for package: ${ packageName }` +- ); +- } +- +- const srcFiles = await glob( +- normalizePath( +- path.join( packageDir, `src/**/*.${ SOURCE_EXTENSIONS }` ) +- ), +- { +- ignore: IGNORE_PATTERNS, +- } +- ); ++ const packageEntry = PACKAGES.get( packageName ); ++ const packageDir = packageEntry.dir; ++ const packageJson = packageEntry.packageJson; ++ ++ const srcFiles = await glob( `src/**/*.${ SOURCE_EXTENSIONS }`, { ++ cwd: packageDir, ++ ignore: IGNORE_PATTERNS, ++ absolute: true, ++ } ); + +- const assetFiles = await glob( +- normalizePath( +- path.join( packageDir, `src/**/*.${ ASSET_EXTENSIONS }` ) +- ), +- { +- ignore: IGNORE_PATTERNS, +- } +- ); ++ const assetFiles = await glob( `src/**/*.${ ASSET_EXTENSIONS }`, { ++ cwd: packageDir, ++ ignore: IGNORE_PATTERNS, ++ absolute: true, ++ } ); + + const buildDir = path.join( packageDir, 'build' ); + const buildModuleDir = path.join( packageDir, 'build-module' ); +@@ -1438,10 +1579,9 @@ async function transpilePackage( packageName ) { + * @return {Promise} Build time in milliseconds, or null if no styles. + */ + async function compileStyles( packageName ) { +- const packageDir = path.join( PACKAGES_DIR, packageName ); +- const packageJson = getPackageInfoFromFile( +- path.join( PACKAGES_DIR, packageName, 'package.json' ) +- ); ++ const packageEntry = PACKAGES.get( packageName ); ++ const packageDir = packageEntry.dir; ++ const packageJson = packageEntry.packageJson; + + // Get SCSS entry point patterns from package.json, default to root-level only + const scssEntryPointPatterns = packageJson.wpStyleEntryPoints || [ +@@ -1549,12 +1689,15 @@ function isPackageSourceFile( filename ) { + return false; + } + +- return PACKAGES.some( ( packageName ) => { ++ for ( const entry of PACKAGES.values() ) { + const packagePath = normalizePath( +- path.join( 'packages', packageName ) ++ path.relative( ROOT_DIR, entry.dir ) + ); +- return relativePath.startsWith( packagePath + '/' ); +- } ); ++ if ( relativePath.startsWith( packagePath + '/' ) ) { ++ return true; ++ } ++ } ++ return false; + } + + /** +@@ -1568,9 +1711,9 @@ function getPackageName( filename ) { + path.relative( process.cwd(), filename ) + ); + +- for ( const packageName of PACKAGES ) { ++ for ( const [ packageName, entry ] of PACKAGES ) { + const packagePath = normalizePath( +- path.join( 'packages', packageName ) ++ path.relative( ROOT_DIR, entry.dir ) + ); + if ( relativePath.startsWith( packagePath + '/' ) ) { + return packageName; +@@ -1733,17 +1876,14 @@ async function buildAll( baseUrlExpression ) { + + const startTime = Date.now(); + +- // Build maps: short name ↔ full name ↔ package.json from package.json files ++ // Build maps: short name ↔ full name ↔ package.json from the registry + const shortToFull = new Map(); + const fullToShort = new Map(); + const fullToPackageJson = new Map(); +- for ( const pkg of PACKAGES ) { +- const packageJson = getPackageInfoFromFile( +- path.join( PACKAGES_DIR, pkg, 'package.json' ) +- ); +- shortToFull.set( pkg, packageJson.name ); +- fullToShort.set( packageJson.name, pkg ); +- fullToPackageJson.set( packageJson.name, packageJson ); ++ for ( const [ pkg, entry ] of PACKAGES ) { ++ shortToFull.set( pkg, entry.packageJson.name ); ++ fullToShort.set( entry.packageJson.name, pkg ); ++ fullToPackageJson.set( entry.packageJson.name, entry.packageJson ); + } + + const levels = groupByDepth( fullToPackageJson ); +@@ -1754,6 +1894,13 @@ async function buildAll( baseUrlExpression ) { + await Promise.all( + level.map( async ( fullName ) => { + const packageName = fullToShort.get( fullName ); ++ const entry = PACKAGES.get( packageName ); ++ ++ // External sources are pre-built — only bundle, skip transpilation. ++ if ( entry.externalSource ) { ++ return; ++ } ++ + const buildTime = await transpilePackage( packageName ); + console.log( + ` ✔ Transpiled ${ packageName } (${ buildTime }ms)` +@@ -1767,7 +1914,7 @@ async function buildAll( baseUrlExpression ) { + const scripts = []; + const styles = []; + await Promise.all( +- PACKAGES.map( async ( packageName ) => { ++ Array.from( PACKAGES.keys() ).map( async ( packageName ) => { + const startBundleTime = Date.now(); + const ret = await bundlePackage( packageName ); + const buildTime = Date.now() - startBundleTime; +@@ -1900,17 +2047,14 @@ async function watchMode() { + let isRebuilding = false; + const needsRebuild = new Set(); + +- // Build maps: short name ↔ full name ↔ package.json from package.json files (once) ++ // Build maps: short name ↔ full name ↔ package.json from the registry (once) + const shortToFull = new Map(); + const fullToShort = new Map(); + const fullToPackageJson = new Map(); +- for ( const pkg of PACKAGES ) { +- const packageJson = getPackageInfoFromFile( +- path.join( PACKAGES_DIR, pkg, 'package.json' ) +- ); +- shortToFull.set( pkg, packageJson.name ); +- fullToShort.set( packageJson.name, pkg ); +- fullToPackageJson.set( packageJson.name, packageJson ); ++ for ( const [ pkg, entry ] of PACKAGES ) { ++ shortToFull.set( pkg, entry.packageJson.name ); ++ fullToShort.set( entry.packageJson.name, pkg ); ++ fullToPackageJson.set( entry.packageJson.name, entry.packageJson ); + } + + // Get all routes for dependency tracking +@@ -1924,8 +2068,13 @@ async function watchMode() { + async function rebuildPackage( packageName ) { + try { + const startTime = Date.now(); ++ const entry = PACKAGES.get( packageName ); ++ ++ // External sources are pre-built — only rebundle. ++ if ( ! entry.externalSource ) { ++ await transpilePackage( packageName ); ++ } + +- await transpilePackage( packageName ); + await bundlePackage( packageName ); + + const buildTime = Date.now() - startTime; +@@ -2021,8 +2170,8 @@ async function watchMode() { + await processNextRebuild(); + } + +- const watchPaths = PACKAGES.map( ( packageName ) => +- path.join( PACKAGES_DIR, packageName, 'src' ) ++ const watchPaths = Array.from( PACKAGES.values() ).map( ( entry ) => ++ path.join( entry.dir, 'src' ) + ); + + const watcher = chokidar.watch( watchPaths, { +diff --git a/lib/package-utils.mjs b/lib/package-utils.mjs +index 15b7dd04c5781a2f9bae022be83702a0a21ea29d..1a66afe14f6a7921fe0c87609bd1c2ca288f3551 100644 +--- a/lib/package-utils.mjs ++++ b/lib/package-utils.mjs +@@ -84,10 +84,39 @@ export function getPackageInfo( fullPackageName, resolveDir = null ) { + return packageJsonCache.get( cacheKey ); + } + +- // Resolve from the package root context to get correct versions + const contextPath = path.join( packageRoot, 'package.json' ); +- const require = createRequire( contextPath ); +- const resolved = require.resolve( `${ fullPackageName }/package.json` ); ++ const localRequire = createRequire( contextPath ); ++ ++ let resolved; ++ try { ++ // Preferred: resolve the package.json subpath directly. ++ resolved = localRequire.resolve( `${ fullPackageName }/package.json` ); ++ } catch { ++ // Fallback for packages whose `exports` field does not expose ++ // `./package.json`. Walk up the directory tree checking each ++ // `node_modules/` — mirrors Node's resolution algorithm without ++ // the exports restriction. ++ let searchDir = packageRoot; ++ const fsRoot = path.parse( searchDir ).root; ++ while ( searchDir !== fsRoot ) { ++ const directPath = path.join( ++ searchDir, ++ 'node_modules', ++ fullPackageName, ++ 'package.json' ++ ); ++ if ( existsSync( directPath ) ) { ++ resolved = directPath; ++ break; ++ } ++ searchDir = path.dirname( searchDir ); ++ } ++ ++ if ( ! resolved ) { ++ return null; ++ } ++ } ++ + const result = getPackageInfoFromFile( resolved ); + packageJsonCache.set( cacheKey, result ); + +diff --git a/lib/wordpress-externals-plugin.mjs b/lib/wordpress-externals-plugin.mjs +index 281749e185b3ec188c4fb93920081869751aab9a..7722b565b42a2ef3cb528949383422a6044206ff 100644 +--- a/lib/wordpress-externals-plugin.mjs ++++ b/lib/wordpress-externals-plugin.mjs +@@ -50,13 +50,15 @@ async function generateContentHash( + * @param {string|false} scriptGlobal Global variable name (e.g., 'wp', 'myPlugin') or false to disable globals. + * @param {Object} externalNamespaces Additional namespaces to externalize (e.g., { 'woo': { global: 'woo', handlePrefix: 'woocommerce' } }). + * @param {string} handlePrefix Handle prefix for main package (e.g., 'wp', 'mp'). Defaults to packageNamespace. ++ * @param {Set} [externalPackages] Individual package names to externalize by exact match (e.g., `@acme/shared-ui`). Used for named `packageSources` entries. + * @return {Function} Function that creates the esbuild plugin instance. + */ + export function createWordpressExternalsPlugin( + packageNamespace, + scriptGlobal, + externalNamespaces = {}, +- handlePrefix ++ handlePrefix, ++ externalPackages = new Set() + ) { + /** + * WordPress externals plugin for esbuild. +@@ -125,6 +127,10 @@ export function createWordpressExternalsPlugin( + const vendorExternals = { + react: { global: 'React', handle: 'react' }, + 'react-dom': { global: 'ReactDOM', handle: 'react-dom' }, ++ 'react-dom/client': { ++ global: 'ReactDOM', ++ handle: 'react-dom', ++ }, + 'react/jsx-runtime': { + global: 'ReactJSXRuntime', + handle: 'react-jsx-runtime', +@@ -277,6 +283,68 @@ export function createWordpressExternalsPlugin( + ); + } + ++ // Handle individual package externals from packageSources. ++ // These match exact package names rather than whole scopes, ++ // avoiding over-broad externalization. ++ for ( const extPkg of externalPackages ) { ++ const escaped = extPkg.replace( ++ /[.*+?^${}()|[\]\\]/g, ++ '\\$&' ++ ); ++ build.onResolve( ++ { filter: new RegExp( `^${ escaped }(/|$)` ) }, ++ /** @param {import('esbuild').OnResolveArgs} args */ ++ ( args ) => { ++ const subpath = ++ args.path.length > extPkg.length ++ ? args.path.slice( extPkg.length + 1 ) ++ : null; ++ ++ const packageJson = getPackageInfo( ++ extPkg, ++ args.resolveDir ++ ); ++ if ( ! packageJson ) { ++ return undefined; ++ } ++ ++ const isScriptModule = isScriptModuleImport( ++ packageJson, ++ subpath ++ ); ++ if ( isScriptModule ) { ++ const kind = ++ args.kind === 'dynamic-import' ++ ? 'dynamic' ++ : 'static'; ++ if ( kind === 'static' ) { ++ moduleDependencies.set( ++ args.path, ++ 'static' ++ ); ++ } else if ( ++ ! moduleDependencies.has( args.path ) ++ ) { ++ moduleDependencies.set( ++ args.path, ++ 'dynamic' ++ ); ++ } ++ ++ return { ++ path: args.path, ++ external: true, ++ sideEffects: !! packageJson.sideEffects, ++ }; ++ } ++ ++ // Not a script module — let esbuild ++ // bundle it inline. ++ return undefined; ++ } ++ ); ++ } ++ + build.onLoad( + { filter: /.*/, namespace: 'vendor-external' }, + /** @param {import('esbuild').OnLoadArgs} args */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78f0bcc91799..cfd8333fbdeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ settings: pnpmfileChecksum: sha256-kvGxuhO5hzJVpfhTkeo4zv3s1CC4nIozwsgvl91BoFw= patchedDependencies: + '@wordpress/build@0.11.0': + hash: 8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203 + path: .pnpm-patches/@wordpress__build@0.11.0.patch react-autosize-textarea: hash: 5c09e1dee59caaaba3871f9d722f93e56b41169db486b059597e8f8c788aa464 path: .pnpm-patches/react-autosize-textarea.patch @@ -2593,7 +2596,7 @@ importers: version: 6.44.0 '@wordpress/build': specifier: 0.11.0 - version: 0.11.0(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(browserslist@4.28.2) + version: 0.11.0(patch_hash=8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(browserslist@4.28.2) '@wordpress/date': specifier: 5.44.0 version: 5.44.0 @@ -3462,12 +3465,18 @@ importers: projects/packages/premium-analytics: dependencies: + '@automattic/number-formatters': + specifier: workspace:* + version: link:../../js-packages/number-formatters '@wordpress/boot': specifier: 0.11.0 - version: 0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + version: 0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) '@wordpress/data': specifier: 10.44.0 version: 10.44.0(react@18.3.1) + '@wordpress/element': + specifier: ^6.22.0 + version: 6.44.0 '@wordpress/i18n': specifier: ^6.9.0 version: 6.16.0 @@ -3487,9 +3496,12 @@ importers: '@babel/core': specifier: 7.29.0 version: 7.29.0 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 '@wordpress/build': specifier: 0.11.0 - version: 0.11.0(@babel/core@7.29.0)(@wordpress/boot@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.11.0(patch_hash=8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: 4.28.2 version: 4.28.2 @@ -23862,6 +23874,40 @@ snapshots: - stylelint - supports-color + '@wordpress/boot@0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0)': + dependencies: + '@wordpress/a11y': 4.44.0 + '@wordpress/admin-ui': 1.12.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/base-styles': 6.20.0 + '@wordpress/commands': 1.44.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/components': 32.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/compose': 7.44.0(react@18.3.1) + '@wordpress/core-data': 7.44.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/data': 10.44.0(react@18.3.1) + '@wordpress/editor': 14.44.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/element': 6.44.0 + '@wordpress/html-entities': 4.44.0 + '@wordpress/i18n': 6.17.0 + '@wordpress/icons': 12.2.0(react@18.3.1) + '@wordpress/keyboard-shortcuts': 5.44.0(react@18.3.1) + '@wordpress/keycodes': 4.44.0 + '@wordpress/lazy-editor': 1.10.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/notices': 5.44.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/primitives': 4.44.0(react@18.3.1) + '@wordpress/private-apis': 1.44.0 + '@wordpress/route': 0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/theme': 0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/url': 4.44.0 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - '@types/react-dom' + - stylelint + - supports-color + '@wordpress/boot@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0)': dependencies: '@wordpress/a11y': 4.44.0 @@ -23898,7 +23944,7 @@ snapshots: '@wordpress/browserslist-config@6.44.0': {} - '@wordpress/build@0.11.0(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(browserslist@4.28.2)': + '@wordpress/build@0.11.0(patch_hash=8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.4.27(postcss@8.5.6) @@ -23924,7 +23970,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.11.0(@babel/core@7.29.0)(@wordpress/boot@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.11.0(patch_hash=8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.4.27(postcss@8.5.6) @@ -23942,7 +23988,7 @@ snapshots: rtlcss: 4.3.0 sass-embedded: 1.97.3 optionalDependencies: - '@wordpress/boot': 0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/boot': 0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) '@wordpress/route': 0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - '@babel/core' @@ -25291,6 +25337,29 @@ snapshots: - stylelint - supports-color + '@wordpress/lazy-editor@1.10.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0)': + dependencies: + '@wordpress/asset-loader': 1.10.0 + '@wordpress/base-styles': 6.20.0 + '@wordpress/block-editor': 15.17.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/blocks': 15.17.0(react@18.3.1) + '@wordpress/components': 32.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/core-data': 7.44.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/data': 10.44.0(react@18.3.1) + '@wordpress/editor': 14.44.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0) + '@wordpress/element': 6.44.0 + '@wordpress/global-styles-engine': 1.11.0(react@18.3.1) + '@wordpress/i18n': 6.17.0 + '@wordpress/private-apis': 1.44.0 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - '@types/react-dom' + - react + - react-dom + - stylelint + - supports-color + '@wordpress/lazy-editor@1.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0)': dependencies: '@wordpress/asset-loader': 1.10.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d42226636e67..81368e1e1ca3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -77,6 +77,10 @@ ignoredOptionalDependencies: # Dependencies needing patching. patchedDependencies: + # Add wpPlugin.packageSources for cross-directory package discovery. + # Upstream PR: https://github.com/WordPress/gutenberg/pull/77226 + '@wordpress/build@0.11.0': .pnpm-patches/@wordpress__build@0.11.0.patch + # Vite/esbuild doesn't like the `__esModule` + `exports["default"]` pattern, it winds up double-wrapping the default. # See also https://github.com/WordPress/gutenberg/issues/39619 react-autosize-textarea: .pnpm-patches/react-autosize-textarea.patch diff --git a/projects/js-packages/number-formatters/changelog/update-wp-build-package-sources b/projects/js-packages/number-formatters/changelog/update-wp-build-package-sources new file mode 100644 index 000000000000..e402479c01c1 --- /dev/null +++ b/projects/js-packages/number-formatters/changelog/update-wp-build-package-sources @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Patch significance. + +Add wpScriptModuleExports field for wp-build script module support diff --git a/projects/js-packages/number-formatters/package.json b/projects/js-packages/number-formatters/package.json index 6932d3902841..141d86cb6a49 100644 --- a/projects/js-packages/number-formatters/package.json +++ b/projects/js-packages/number-formatters/package.json @@ -24,6 +24,7 @@ "main": "./dist/cjs/index.cjs", "module": "./dist/esm/index.js", "types": "./dist/types/index.d.ts", + "wpScriptModuleExports": ".", "scripts": { "build": "pnpm run clean && pnpm run build:ts", "build:ts": "duel --dirs", From 1b3fb871a8f413071ae18aed2826c34642672c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 17 Apr 2026 16:59:41 +0100 Subject: [PATCH 2/8] adopt packageSources in premium-analytics configures @automattic/number-formatters as a named packageSource and demonstrates both import modes in the dashboard route: static import for formatNumber, dynamic import for formatNumberCompact via React.lazy and Suspense. --- .../changelog/update-wp-build-package-sources | 4 +++ .../packages/premium-analytics/package.json | 6 ++++ .../routes/dashboard/package.json | 3 ++ .../routes/dashboard/stage.tsx | 28 +++++++++++++++++-- 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 projects/packages/premium-analytics/changelog/update-wp-build-package-sources diff --git a/projects/packages/premium-analytics/changelog/update-wp-build-package-sources b/projects/packages/premium-analytics/changelog/update-wp-build-package-sources new file mode 100644 index 000000000000..2bd3c476d8cb --- /dev/null +++ b/projects/packages/premium-analytics/changelog/update-wp-build-package-sources @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Add packageSources support and number-formatters integration. diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index 9cad7d6a02f5..66833cca6009 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -20,6 +20,9 @@ ] } ], + "packageSources": [ + "@automattic/number-formatters" + ], "externalNamespaces": { "wordpress": { "global": "wp", @@ -28,8 +31,10 @@ } }, "dependencies": { + "@automattic/number-formatters": "workspace:*", "@wordpress/boot": "0.11.0", "@wordpress/data": "10.44.0", + "@wordpress/element": "^6.22.0", "@wordpress/i18n": "^6.9.0", "@wordpress/icons": "^12.0.0", "@wordpress/route": "0.10.0", @@ -38,6 +43,7 @@ }, "devDependencies": { "@babel/core": "7.29.0", + "@types/react": "18.3.28", "@wordpress/build": "0.11.0", "browserslist": "4.28.2" } diff --git a/projects/packages/premium-analytics/routes/dashboard/package.json b/projects/packages/premium-analytics/routes/dashboard/package.json index e71390452278..6fc64ed66e91 100644 --- a/projects/packages/premium-analytics/routes/dashboard/package.json +++ b/projects/packages/premium-analytics/routes/dashboard/package.json @@ -6,6 +6,9 @@ "page": "jetpack-premium-analytics" }, "dependencies": { + "@automattic/number-formatters": "workspace:*", + "@types/react": "18.3.28", + "@wordpress/element": "^6.22.0", "@wordpress/i18n": "^6.9.0" } } diff --git a/projects/packages/premium-analytics/routes/dashboard/stage.tsx b/projects/packages/premium-analytics/routes/dashboard/stage.tsx index 034a332bc4eb..450172717251 100644 --- a/projects/packages/premium-analytics/routes/dashboard/stage.tsx +++ b/projects/packages/premium-analytics/routes/dashboard/stage.tsx @@ -1,10 +1,34 @@ +import { formatNumber } from '@automattic/number-formatters'; +import { lazy, Suspense } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -export const stage = () => { +const samplePageViews = 1234567; +const sampleUniqueVisitors = 98432; + +const UniqueVisitors = lazy( () => + import( '@automattic/number-formatters' ).then( ( { formatNumberCompact } ) => ( { + default: ( { value }: { value: number } ) => { formatNumberCompact( value ) }, + } ) ) +); + +const Stage = () => { return (

{ __( 'Analytics', 'jetpack-premium-analytics' ) }

-

{ __( 'Welcome to the Analytics dashboard.', 'jetpack-premium-analytics' ) }

+ +

+ { __( 'Unique visitors (dynamic):', 'jetpack-premium-analytics' ) }{ ' ' } + … }> + + +

+ +

+ { __( 'Total page views (static):', 'jetpack-premium-analytics' ) }{ ' ' } + { formatNumber( samplePageViews ) } +

); }; + +export { Stage as stage }; From fd398fc407f523ffa56820f4184fe593652d48e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Sat, 18 Apr 2026 12:56:48 +0100 Subject: [PATCH 3/8] rebase patch to @wordpress/build@0.12.0 regenerate patch against 0.12.0 sources using upstream PR #77226 diff. header embeds the version-agnostic rebase procedure. --- ...0.patch => @wordpress__build@0.12.0.patch} | 107 +++++++----------- pnpm-lock.yaml | 35 ++++-- pnpm-workspace.yaml | 2 +- .../packages/premium-analytics/package.json | 2 +- 4 files changed, 67 insertions(+), 79 deletions(-) rename .pnpm-patches/{@wordpress__build@0.11.0.patch => @wordpress__build@0.12.0.patch} (87%) diff --git a/.pnpm-patches/@wordpress__build@0.11.0.patch b/.pnpm-patches/@wordpress__build@0.12.0.patch similarity index 87% rename from .pnpm-patches/@wordpress__build@0.11.0.patch rename to .pnpm-patches/@wordpress__build@0.12.0.patch index aa7deef39aa7..c4386f217b4b 100644 --- a/.pnpm-patches/@wordpress__build@0.11.0.patch +++ b/.pnpm-patches/@wordpress__build@0.12.0.patch @@ -1,24 +1,33 @@ Bridge to upstream Gutenberg PR https://github.com/WordPress/gutenberg/pull/77226. Adds `wpPlugin.packageSources` to @wordpress/build for cross-directory package discovery. -When the upstream PR merges and Jetpack bumps @wordpress/build, -delete this file and remove its entry from pnpm-workspace.yaml. +When the upstream PR merges and Jetpack bumps @wordpress/build to a version that +includes it, delete this file and remove its entry from pnpm-workspace.yaml. -To regenerate after upstream changes (requires Gutenberg checkout with the feature branch): +To rebase this patch to a new @wordpress/build version, without a Gutenberg checkout: - PATCH=.pnpm-patches/@wordpress__build@0.11.0.patch - { - sed '/^diff --git/,$d' "$PATCH" - git -C ~/lab/gutenberg diff --full-index \ - @wordpress/build@0.11.0..add/wp-build-sources-config -- \ - packages/wp-build/lib/build.mjs \ - packages/wp-build/lib/package-utils.mjs \ - packages/wp-build/lib/wordpress-externals-plugin.mjs \ - | sed -e 's|a/packages/wp-build/|a/|g' -e 's|b/packages/wp-build/|b/|g' - } > "$PATCH.new" && mv "$PATCH.new" "$PATCH" + VERSION=0.13.0 + WORK=$(mktemp -d) && cd "$WORK" + curl -sL "https://registry.npmjs.org/@wordpress/build/-/build-$VERSION.tgz" | tar xz + mv package pristine && cp -R pristine patched + curl -sL https://patch-diff.githubusercontent.com/raw/WordPress/gutenberg/pull/77226.diff \ + | awk '/^diff --git/{f=($4 ~ /^b\/packages\/wp-build\/lib\//)} f' \ + | sed 's|/packages/wp-build/|/|g' \ + | (cd patched && patch -p1) + find patched -name '*.orig' -delete + git diff --no-index --no-color pristine patched \ + | sed -E -e 's|^diff --git a/pristine/|diff --git a/|' \ + -e 's| b/patched/| b/|' \ + -e 's|^--- a/pristine/|--- a/|' \ + -e 's|^\+\+\+ b/patched/|+++ b/|' \ + > "$OLDPWD/.pnpm-patches/@wordpress__build@$VERSION.patch" + +Then prepend this header, update pnpm-workspace.yaml, delete the old patch file, +and run `pnpm install` + `pnpm --filter @automattic/jetpack-premium-analytics build` +to validate. diff --git a/lib/build.mjs b/lib/build.mjs -index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420383a0d96 100755 +index 628cf02..d0bf9a8 100755 --- a/lib/build.mjs +++ b/lib/build.mjs @@ -4,6 +4,7 @@ @@ -280,7 +289,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 for ( const scriptHandle of scriptDependencies ) { // Skip non-package dependencies (like 'react', 'lodash', etc.) -@@ -1215,34 +1369,21 @@ async function generatePagesPhp( pageData, replacements ) { +@@ -1215,16 +1369,9 @@ async function generatePagesPhp( pageData, replacements ) { */ async function transpilePackage( packageName ) { const startTime = Date.now(); @@ -294,42 +303,13 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 - `Could not find package.json for package: ${ packageName }` - ); - } -- -- const srcFiles = await glob( -- normalizePath( -- path.join( packageDir, `src/**/*.${ SOURCE_EXTENSIONS }` ) -- ), -- { -- ignore: IGNORE_PATTERNS, -- } -- ); + const packageEntry = PACKAGES.get( packageName ); + const packageDir = packageEntry.dir; + const packageJson = packageEntry.packageJson; -+ -+ const srcFiles = await glob( `src/**/*.${ SOURCE_EXTENSIONS }`, { -+ cwd: packageDir, -+ ignore: IGNORE_PATTERNS, -+ absolute: true, -+ } ); - -- const assetFiles = await glob( -- normalizePath( -- path.join( packageDir, `src/**/*.${ ASSET_EXTENSIONS }` ) -- ), -- { -- ignore: IGNORE_PATTERNS, -- } -- ); -+ const assetFiles = await glob( `src/**/*.${ ASSET_EXTENSIONS }`, { -+ cwd: packageDir, -+ ignore: IGNORE_PATTERNS, -+ absolute: true, -+ } ); - const buildDir = path.join( packageDir, 'build' ); - const buildModuleDir = path.join( packageDir, 'build-module' ); -@@ -1438,10 +1579,9 @@ async function transpilePackage( packageName ) { + const srcFiles = await glob( `src/**/*.${ SOURCE_EXTENSIONS }`, { + cwd: packageDir, +@@ -1432,10 +1579,9 @@ async function transpilePackage( packageName ) { * @return {Promise} Build time in milliseconds, or null if no styles. */ async function compileStyles( packageName ) { @@ -343,7 +323,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 // Get SCSS entry point patterns from package.json, default to root-level only const scssEntryPointPatterns = packageJson.wpStyleEntryPoints || [ -@@ -1549,12 +1689,15 @@ function isPackageSourceFile( filename ) { +@@ -1543,12 +1689,15 @@ function isPackageSourceFile( filename ) { return false; } @@ -363,7 +343,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 } /** -@@ -1568,9 +1711,9 @@ function getPackageName( filename ) { +@@ -1562,9 +1711,9 @@ function getPackageName( filename ) { path.relative( process.cwd(), filename ) ); @@ -375,7 +355,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 ); if ( relativePath.startsWith( packagePath + '/' ) ) { return packageName; -@@ -1733,17 +1876,14 @@ async function buildAll( baseUrlExpression ) { +@@ -1727,17 +1876,14 @@ async function buildAll( baseUrlExpression ) { const startTime = Date.now(); @@ -398,7 +378,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 } const levels = groupByDepth( fullToPackageJson ); -@@ -1754,6 +1894,13 @@ async function buildAll( baseUrlExpression ) { +@@ -1748,6 +1894,13 @@ async function buildAll( baseUrlExpression ) { await Promise.all( level.map( async ( fullName ) => { const packageName = fullToShort.get( fullName ); @@ -412,7 +392,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 const buildTime = await transpilePackage( packageName ); console.log( ` ✔ Transpiled ${ packageName } (${ buildTime }ms)` -@@ -1767,7 +1914,7 @@ async function buildAll( baseUrlExpression ) { +@@ -1761,7 +1914,7 @@ async function buildAll( baseUrlExpression ) { const scripts = []; const styles = []; await Promise.all( @@ -421,7 +401,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 const startBundleTime = Date.now(); const ret = await bundlePackage( packageName ); const buildTime = Date.now() - startBundleTime; -@@ -1900,17 +2047,14 @@ async function watchMode() { +@@ -1894,17 +2047,14 @@ async function watchMode() { let isRebuilding = false; const needsRebuild = new Set(); @@ -444,7 +424,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 } // Get all routes for dependency tracking -@@ -1924,8 +2068,13 @@ async function watchMode() { +@@ -1918,8 +2068,13 @@ async function watchMode() { async function rebuildPackage( packageName ) { try { const startTime = Date.now(); @@ -459,7 +439,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 await bundlePackage( packageName ); const buildTime = Date.now() - startTime; -@@ -2021,8 +2170,8 @@ async function watchMode() { +@@ -2015,8 +2170,8 @@ async function watchMode() { await processNextRebuild(); } @@ -471,7 +451,7 @@ index 5fa8d66b5e6481af02fa03dd3c8587fcc533a6a5..d0bf9a897c1afed10a5673c225b7d420 const watcher = chokidar.watch( watchPaths, { diff --git a/lib/package-utils.mjs b/lib/package-utils.mjs -index 15b7dd04c5781a2f9bae022be83702a0a21ea29d..1a66afe14f6a7921fe0c87609bd1c2ca288f3551 100644 +index 15b7dd0..1a66afe 100644 --- a/lib/package-utils.mjs +++ b/lib/package-utils.mjs @@ -84,10 +84,39 @@ export function getPackageInfo( fullPackageName, resolveDir = null ) { @@ -518,7 +498,7 @@ index 15b7dd04c5781a2f9bae022be83702a0a21ea29d..1a66afe14f6a7921fe0c87609bd1c2ca packageJsonCache.set( cacheKey, result ); diff --git a/lib/wordpress-externals-plugin.mjs b/lib/wordpress-externals-plugin.mjs -index 281749e185b3ec188c4fb93920081869751aab9a..7722b565b42a2ef3cb528949383422a6044206ff 100644 +index 281749e..04bdc63 100644 --- a/lib/wordpress-externals-plugin.mjs +++ b/lib/wordpress-externals-plugin.mjs @@ -50,13 +50,15 @@ async function generateContentHash( @@ -538,18 +518,7 @@ index 281749e185b3ec188c4fb93920081869751aab9a..7722b565b42a2ef3cb528949383422a6 ) { /** * WordPress externals plugin for esbuild. -@@ -125,6 +127,10 @@ export function createWordpressExternalsPlugin( - const vendorExternals = { - react: { global: 'React', handle: 'react' }, - 'react-dom': { global: 'ReactDOM', handle: 'react-dom' }, -+ 'react-dom/client': { -+ global: 'ReactDOM', -+ handle: 'react-dom', -+ }, - 'react/jsx-runtime': { - global: 'ReactJSXRuntime', - handle: 'react-jsx-runtime', -@@ -277,6 +283,68 @@ export function createWordpressExternalsPlugin( +@@ -277,6 +279,68 @@ export function createWordpressExternalsPlugin( ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfd8333fbdeb..d3e0c7dc07fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,9 +7,9 @@ settings: pnpmfileChecksum: sha256-kvGxuhO5hzJVpfhTkeo4zv3s1CC4nIozwsgvl91BoFw= patchedDependencies: - '@wordpress/build@0.11.0': - hash: 8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203 - path: .pnpm-patches/@wordpress__build@0.11.0.patch + '@wordpress/build@0.12.0': + hash: c7ed2a87c1bf3660a1ac519f4c183b672d38dfe7bd3422dbc5b258ac52c276ee + path: .pnpm-patches/@wordpress__build@0.12.0.patch react-autosize-textarea: hash: 5c09e1dee59caaaba3871f9d722f93e56b41169db486b059597e8f8c788aa464 path: .pnpm-patches/react-autosize-textarea.patch @@ -2596,7 +2596,7 @@ importers: version: 6.44.0 '@wordpress/build': specifier: 0.11.0 - version: 0.11.0(patch_hash=8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(browserslist@4.28.2) + version: 0.11.0(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(browserslist@4.28.2) '@wordpress/date': specifier: 5.44.0 version: 5.44.0 @@ -3500,8 +3500,8 @@ importers: specifier: 18.3.28 version: 18.3.28 '@wordpress/build': - specifier: 0.11.0 - version: 0.11.0(patch_hash=8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + specifier: 0.12.0 + version: 0.12.0(patch_hash=c7ed2a87c1bf3660a1ac519f4c183b672d38dfe7bd3422dbc5b258ac52c276ee)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: 4.28.2 version: 4.28.2 @@ -10463,6 +10463,25 @@ packages: '@wordpress/theme': optional: true + '@wordpress/build@0.12.0': + resolution: {integrity: sha512-PCwxVXEKGVjwZRRVGhl6jaiOXX4y3ENsUj7UFKFPC9Nna6ov9YOQvhM+1+87Wvqqlze9jAOAMU0cuUQ+WmntjQ==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + hasBin: true + peerDependencies: + '@wordpress/boot': '>=0.3.0' + '@wordpress/private-apis': ^1.0.0 + '@wordpress/route': '>=0.2.0' + '@wordpress/theme': '>=0.3.0' + peerDependenciesMeta: + '@wordpress/boot': + optional: true + '@wordpress/private-apis': + optional: true + '@wordpress/route': + optional: true + '@wordpress/theme': + optional: true + '@wordpress/commands@1.44.0': resolution: {integrity: sha512-/e+ef0ahEgF55M0UrVfUEuOEQ4OeBZZde8wEUyTIqOB0gtv9gwG5VKOuzCH1kK3gWLdQd9YWt6NWUrc38mLWsw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -23944,7 +23963,7 @@ snapshots: '@wordpress/browserslist-config@6.44.0': {} - '@wordpress/build@0.11.0(patch_hash=8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(browserslist@4.28.2)': + '@wordpress/build@0.11.0(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.11.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.4.27(postcss@8.5.6) @@ -23970,7 +23989,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.11.0(patch_hash=8e71ddd386b32d785bf2f18c8f23360675aa239ee6c1f78cc9536ab3975a9203)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.12.0(patch_hash=c7ed2a87c1bf3660a1ac519f4c183b672d38dfe7bd3422dbc5b258ac52c276ee)(@babel/core@7.29.0)(@wordpress/boot@0.11.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0))(@wordpress/route@0.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.4.27(postcss@8.5.6) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 81368e1e1ca3..04c39695223e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -79,7 +79,7 @@ patchedDependencies: # Add wpPlugin.packageSources for cross-directory package discovery. # Upstream PR: https://github.com/WordPress/gutenberg/pull/77226 - '@wordpress/build@0.11.0': .pnpm-patches/@wordpress__build@0.11.0.patch + '@wordpress/build@0.12.0': .pnpm-patches/@wordpress__build@0.12.0.patch # Vite/esbuild doesn't like the `__esModule` + `exports["default"]` pattern, it winds up double-wrapping the default. # See also https://github.com/WordPress/gutenberg/issues/39619 diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index 66833cca6009..53e3db34390d 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@babel/core": "7.29.0", "@types/react": "18.3.28", - "@wordpress/build": "0.11.0", + "@wordpress/build": "0.12.0", "browserslist": "4.28.2" } } From bfd9364fd4b2e5c2894b999b6ad898cabe81c5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Sat, 30 May 2026 20:09:00 +0100 Subject: [PATCH 4/8] update pnpm patch to mirror upstream PR #78822 Replaces the @wordpress/build@0.12.0 packageSources patch with a 0.13.0 patch that mirrors the new upstream proposal: identity from package.json#name, discovery via convention (dependencies that declare wpScriptModuleExports), plus a node_modules walk-up fallback in getPackageInfo for packages whose exports field hides ./package.json. No more wpPlugin.packageSources config needed on the consumer side. --- .pnpm-patches/@wordpress__build@0.12.0.patch | 589 --------------- .pnpm-patches/@wordpress__build@0.13.0.patch | 702 ++++++++++++++++++ pnpm-workspace.yaml | 7 +- .../changelog/update-wp-build-package-sources | 2 +- 4 files changed, 707 insertions(+), 593 deletions(-) delete mode 100644 .pnpm-patches/@wordpress__build@0.12.0.patch create mode 100644 .pnpm-patches/@wordpress__build@0.13.0.patch diff --git a/.pnpm-patches/@wordpress__build@0.12.0.patch b/.pnpm-patches/@wordpress__build@0.12.0.patch deleted file mode 100644 index c4386f217b4b..000000000000 --- a/.pnpm-patches/@wordpress__build@0.12.0.patch +++ /dev/null @@ -1,589 +0,0 @@ -Bridge to upstream Gutenberg PR https://github.com/WordPress/gutenberg/pull/77226. -Adds `wpPlugin.packageSources` to @wordpress/build for cross-directory package discovery. - -When the upstream PR merges and Jetpack bumps @wordpress/build to a version that -includes it, delete this file and remove its entry from pnpm-workspace.yaml. - -To rebase this patch to a new @wordpress/build version, without a Gutenberg checkout: - - VERSION=0.13.0 - WORK=$(mktemp -d) && cd "$WORK" - curl -sL "https://registry.npmjs.org/@wordpress/build/-/build-$VERSION.tgz" | tar xz - mv package pristine && cp -R pristine patched - curl -sL https://patch-diff.githubusercontent.com/raw/WordPress/gutenberg/pull/77226.diff \ - | awk '/^diff --git/{f=($4 ~ /^b\/packages\/wp-build\/lib\//)} f' \ - | sed 's|/packages/wp-build/|/|g' \ - | (cd patched && patch -p1) - find patched -name '*.orig' -delete - git diff --no-index --no-color pristine patched \ - | sed -E -e 's|^diff --git a/pristine/|diff --git a/|' \ - -e 's| b/patched/| b/|' \ - -e 's|^--- a/pristine/|--- a/|' \ - -e 's|^\+\+\+ b/patched/|+++ b/|' \ - > "$OLDPWD/.pnpm-patches/@wordpress__build@$VERSION.patch" - -Then prepend this header, update pnpm-workspace.yaml, delete the old patch file, -and run `pnpm install` + `pnpm --filter @automattic/jetpack-premium-analytics build` -to validate. - -diff --git a/lib/build.mjs b/lib/build.mjs -index 628cf02..d0bf9a8 100755 ---- a/lib/build.mjs -+++ b/lib/build.mjs -@@ -4,6 +4,7 @@ - * External dependencies - */ - import { readFile, writeFile, copyFile, mkdir, unlink } from 'fs/promises'; -+import { existsSync } from 'fs'; - import path from 'path'; - import { createHash } from 'node:crypto'; - import { parseArgs } from 'node:util'; -@@ -90,29 +91,171 @@ const TEST_FILE_PATTERNS = [ - ]; - - /** -- * Get all package names from the packages directory. -+ * A discovered package in the registry. - * -- * @return {string[]} Array of package names. -+ * @typedef {Object} PackageEntry -+ * @property {string} dir Absolute path to the package directory. -+ * @property {import('./package-utils.mjs').PackageJson} packageJson Parsed package.json contents. -+ * @property {boolean} [externalSource] True when the entry comes from a named -+ * package source (e.g. `@scope/name`). -+ * These packages preserve their own npm -+ * identity for script-module IDs instead -+ * of being scoped under `packageNamespace`. - */ --function getAllPackages() { -- return glob -- .sync( normalizePath( path.join( PACKAGES_DIR, '*', 'package.json' ) ) ) -- .map( ( packageJsonPath ) => -- path.basename( path.dirname( packageJsonPath ) ) -- ); --} - --const PACKAGES = getAllPackages(); - const ROOT_PACKAGE_JSON = getPackageInfoFromFile( - path.join( ROOT_DIR, 'package.json' ) - ); - const WP_PLUGIN_CONFIG = ROOT_PACKAGE_JSON.wpPlugin || {}; -+ -+/** -+ * Check whether a sources entry is a npm package name rather than a -+ * relative/absolute directory path. -+ * -+ * Package names follow the npm naming rules: -+ * - Scoped: `@scope/name` -+ * - Bare: `my-package` (starts with a letter or digit, no path separators) -+ * -+ * Directory paths start with `.`, `/`, or a drive letter on Windows (`C:\`). -+ * -+ * @param {string} source A single entry from `wpPlugin.packageSources`. -+ * @return {boolean} True when the entry looks like a package name. -+ */ -+function isPackageName( source ) { -+ if ( source.startsWith( '@' ) ) { -+ return true; -+ } -+ // Relative or absolute paths. -+ if ( source.startsWith( '.' ) || path.isAbsolute( source ) ) { -+ return false; -+ } -+ // Bare package name (no slashes → not a path). -+ return ! source.includes( '/' ); -+} -+ -+/** -+ * Resolve a npm package name to its directory and parsed package.json. -+ * Uses Node's module resolution from the project root context so that -+ * workspace symlinks (pnpm, yarn, npm) are followed automatically. -+ * -+ * @param {string} npmName Full package name (e.g. `@acme/shared-ui`). -+ * @return {{ dir: string, packageJson: import('./package-utils.mjs').PackageJson }|null} -+ * Resolved entry or null when the package is not resolvable. -+ */ -+function resolveNamedSource( npmName ) { -+ // Read directly from node_modules instead of require.resolve() to -+ // avoid ERR_PACKAGE_PATH_NOT_EXPORTED when the package's `exports` -+ // field does not include `./package.json`. -+ const pkgJsonPath = path.join( -+ ROOT_DIR, -+ 'node_modules', -+ npmName, -+ 'package.json' -+ ); -+ -+ if ( ! existsSync( pkgJsonPath ) ) { -+ console.warn( -+ `⚠️ Source "${ npmName }" could not be resolved. ` + -+ 'Make sure it is listed in dependencies and installed.' -+ ); -+ return null; -+ } -+ -+ return { -+ dir: path.dirname( pkgJsonPath ), -+ packageJson: getPackageInfoFromFile( pkgJsonPath ), -+ }; -+} -+ -+/** -+ * Directories to scan for packages. Always starts with `./packages/`. -+ * Additional directory-type entries from `wpPlugin.packageSources` are -+ * appended. Package-name entries are handled separately in -+ * `getAllPackages()`. -+ * -+ * @type {string[]} -+ */ -+const PACKAGE_SOURCES = WP_PLUGIN_CONFIG.packageSources || []; -+const PACKAGE_DIRS = [ -+ PACKAGES_DIR, -+ ...PACKAGE_SOURCES.filter( ( s ) => ! isPackageName( s ) ).map( ( s ) => -+ path.resolve( ROOT_DIR, s ) -+ ), -+]; -+const NAMED_SOURCES = PACKAGE_SOURCES.filter( isPackageName ); -+ -+/** -+ * Get all packages by scanning every directory in PACKAGE_DIRS and -+ * resolving every named source in NAMED_SOURCES. -+ * -+ * Local packages (`./packages/`) are scanned first, so they take priority. -+ * Named sources are resolved last — they preserve their npm identity -+ * (e.g. `@acme/shared-ui` stays `@acme/shared-ui` in script-module -+ * IDs instead of being rewritten to `@/shared-ui`). -+ * -+ * @return {Map} Map of package names to their entry data. -+ */ -+function getAllPackages() { -+ const registry = new Map(); -+ -+ // 1. Directory-based discovery (local packages first, then source dirs). -+ for ( const dir of PACKAGE_DIRS ) { -+ const pkgJsonPaths = glob.sync( -+ normalizePath( path.join( dir, '*', 'package.json' ) ) -+ ); -+ -+ for ( const pkgJsonPath of pkgJsonPaths ) { -+ const name = path.basename( path.dirname( pkgJsonPath ) ); -+ // First match wins — local packages take priority over -+ // sources-discovered packages. -+ if ( ! registry.has( name ) ) { -+ registry.set( name, { -+ dir: path.dirname( pkgJsonPath ), -+ packageJson: getPackageInfoFromFile( pkgJsonPath ), -+ } ); -+ } -+ } -+ } -+ -+ // 2. Named sources — resolve via require.resolve() and preserve -+ // the full npm name as the registry key. -+ for ( const npmName of NAMED_SOURCES ) { -+ if ( registry.has( npmName ) ) { -+ continue; -+ } -+ -+ const entry = resolveNamedSource( npmName ); -+ if ( entry ) { -+ if ( ! entry.packageJson.wpScriptModuleExports ) { -+ console.warn( -+ `⚠️ Source "${ npmName }" does not declare wpScriptModuleExports. ` + -+ 'Imports will be bundled inline instead of externalized.' -+ ); -+ } -+ -+ registry.set( npmName, { -+ ...entry, -+ externalSource: true, -+ } ); -+ } -+ } -+ -+ return registry; -+} -+ -+const PACKAGES = getAllPackages(); - const SCRIPT_GLOBAL = WP_PLUGIN_CONFIG.scriptGlobal; - const PACKAGE_NAMESPACE = WP_PLUGIN_CONFIG.packageNamespace; - const HANDLE_PREFIX = WP_PLUGIN_CONFIG.handlePrefix || PACKAGE_NAMESPACE; --const EXTERNAL_NAMESPACES = WP_PLUGIN_CONFIG.externalNamespaces || {}; - const PAGES = WP_PLUGIN_CONFIG.pages || []; - -+const EXTERNAL_NAMESPACES = WP_PLUGIN_CONFIG.externalNamespaces || {}; -+ -+// Individual packages from named sources to externalize. Unlike -+// EXTERNAL_NAMESPACES (which externalizes an entire `@scope/*`), this -+// targets only the exact packages listed in `packageSources`. -+const EXTERNAL_PACKAGES = new Set( NAMED_SOURCES ); -+ - /** - * Interprets a configuration value as a boolean, where `"true"` and `"1"` - * are considered true while all other values are false. -@@ -153,7 +296,8 @@ const wordpressExternalsPlugin = createWordpressExternalsPlugin( - PACKAGE_NAMESPACE, - SCRIPT_GLOBAL, - EXTERNAL_NAMESPACES, -- HANDLE_PREFIX -+ HANDLE_PREFIX, -+ EXTERNAL_PACKAGES - ); - - /** -@@ -480,7 +624,6 @@ function resolveEntryPoint( packageDir, packageJson ) { - */ - async function bundlePackage( packageName, options = {} ) { - const { -- sourceDir = PACKAGES_DIR, - handlePrefix = HANDLE_PREFIX, - scriptGlobal = SCRIPT_GLOBAL, - packageNamespace = PACKAGE_NAMESPACE, -@@ -489,10 +632,10 @@ async function bundlePackage( packageName, options = {} ) { - const builtModules = []; - const builtScripts = []; - const builtStyles = []; -- const packageDir = path.join( sourceDir, packageName ); -- const packageJson = getPackageInfoFromFile( -- path.join( sourceDir, packageName, 'package.json' ) -- ); -+ const packageEntry = PACKAGES.get( packageName ); -+ const packageDir = packageEntry.dir; -+ const packageJson = packageEntry.packageJson; -+ const isExternalSource = !! packageEntry.externalSource; - - const builds = []; - -@@ -653,10 +796,21 @@ async function bundlePackage( packageName, options = {} ) { - ); - } - -- const scriptModuleId = -- exportName === '.' -- ? `@${ packageNamespace }/${ packageName }` -- : `@${ packageNamespace }/${ packageName }/${ fileName }`; -+ // External sources preserve their npm identity as the -+ // script-module ID (e.g. `@acme/shared-ui`). Local -+ // packages are scoped under the plugin's namespace. -+ let scriptModuleId; -+ if ( isExternalSource ) { -+ scriptModuleId = -+ exportName === '.' -+ ? packageName -+ : `${ packageName }/${ fileName }`; -+ } else { -+ scriptModuleId = -+ exportName === '.' -+ ? `@${ packageNamespace }/${ packageName }` -+ : `@${ packageNamespace }/${ packageName }/${ fileName }`; -+ } - - builtModules.push( { - id: scriptModuleId, -@@ -862,7 +1016,7 @@ async function inferStyleDependencies( scriptDependencies, packageName ) { - - const styleDeps = []; - // Get the resolve directory for context-aware package resolution -- const resolveDir = path.join( PACKAGES_DIR, packageName ); -+ const resolveDir = PACKAGES.get( packageName ).dir; - - for ( const scriptHandle of scriptDependencies ) { - // Skip non-package dependencies (like 'react', 'lodash', etc.) -@@ -1215,16 +1369,9 @@ async function generatePagesPhp( pageData, replacements ) { - */ - async function transpilePackage( packageName ) { - const startTime = Date.now(); -- const packageDir = path.join( PACKAGES_DIR, packageName ); -- const packageJson = getPackageInfoFromFile( -- path.join( PACKAGES_DIR, packageName, 'package.json' ) -- ); -- -- if ( ! packageJson ) { -- throw new Error( -- `Could not find package.json for package: ${ packageName }` -- ); -- } -+ const packageEntry = PACKAGES.get( packageName ); -+ const packageDir = packageEntry.dir; -+ const packageJson = packageEntry.packageJson; - - const srcFiles = await glob( `src/**/*.${ SOURCE_EXTENSIONS }`, { - cwd: packageDir, -@@ -1432,10 +1579,9 @@ async function transpilePackage( packageName ) { - * @return {Promise} Build time in milliseconds, or null if no styles. - */ - async function compileStyles( packageName ) { -- const packageDir = path.join( PACKAGES_DIR, packageName ); -- const packageJson = getPackageInfoFromFile( -- path.join( PACKAGES_DIR, packageName, 'package.json' ) -- ); -+ const packageEntry = PACKAGES.get( packageName ); -+ const packageDir = packageEntry.dir; -+ const packageJson = packageEntry.packageJson; - - // Get SCSS entry point patterns from package.json, default to root-level only - const scssEntryPointPatterns = packageJson.wpStyleEntryPoints || [ -@@ -1543,12 +1689,15 @@ function isPackageSourceFile( filename ) { - return false; - } - -- return PACKAGES.some( ( packageName ) => { -+ for ( const entry of PACKAGES.values() ) { - const packagePath = normalizePath( -- path.join( 'packages', packageName ) -+ path.relative( ROOT_DIR, entry.dir ) - ); -- return relativePath.startsWith( packagePath + '/' ); -- } ); -+ if ( relativePath.startsWith( packagePath + '/' ) ) { -+ return true; -+ } -+ } -+ return false; - } - - /** -@@ -1562,9 +1711,9 @@ function getPackageName( filename ) { - path.relative( process.cwd(), filename ) - ); - -- for ( const packageName of PACKAGES ) { -+ for ( const [ packageName, entry ] of PACKAGES ) { - const packagePath = normalizePath( -- path.join( 'packages', packageName ) -+ path.relative( ROOT_DIR, entry.dir ) - ); - if ( relativePath.startsWith( packagePath + '/' ) ) { - return packageName; -@@ -1727,17 +1876,14 @@ async function buildAll( baseUrlExpression ) { - - const startTime = Date.now(); - -- // Build maps: short name ↔ full name ↔ package.json from package.json files -+ // Build maps: short name ↔ full name ↔ package.json from the registry - const shortToFull = new Map(); - const fullToShort = new Map(); - const fullToPackageJson = new Map(); -- for ( const pkg of PACKAGES ) { -- const packageJson = getPackageInfoFromFile( -- path.join( PACKAGES_DIR, pkg, 'package.json' ) -- ); -- shortToFull.set( pkg, packageJson.name ); -- fullToShort.set( packageJson.name, pkg ); -- fullToPackageJson.set( packageJson.name, packageJson ); -+ for ( const [ pkg, entry ] of PACKAGES ) { -+ shortToFull.set( pkg, entry.packageJson.name ); -+ fullToShort.set( entry.packageJson.name, pkg ); -+ fullToPackageJson.set( entry.packageJson.name, entry.packageJson ); - } - - const levels = groupByDepth( fullToPackageJson ); -@@ -1748,6 +1894,13 @@ async function buildAll( baseUrlExpression ) { - await Promise.all( - level.map( async ( fullName ) => { - const packageName = fullToShort.get( fullName ); -+ const entry = PACKAGES.get( packageName ); -+ -+ // External sources are pre-built — only bundle, skip transpilation. -+ if ( entry.externalSource ) { -+ return; -+ } -+ - const buildTime = await transpilePackage( packageName ); - console.log( - ` ✔ Transpiled ${ packageName } (${ buildTime }ms)` -@@ -1761,7 +1914,7 @@ async function buildAll( baseUrlExpression ) { - const scripts = []; - const styles = []; - await Promise.all( -- PACKAGES.map( async ( packageName ) => { -+ Array.from( PACKAGES.keys() ).map( async ( packageName ) => { - const startBundleTime = Date.now(); - const ret = await bundlePackage( packageName ); - const buildTime = Date.now() - startBundleTime; -@@ -1894,17 +2047,14 @@ async function watchMode() { - let isRebuilding = false; - const needsRebuild = new Set(); - -- // Build maps: short name ↔ full name ↔ package.json from package.json files (once) -+ // Build maps: short name ↔ full name ↔ package.json from the registry (once) - const shortToFull = new Map(); - const fullToShort = new Map(); - const fullToPackageJson = new Map(); -- for ( const pkg of PACKAGES ) { -- const packageJson = getPackageInfoFromFile( -- path.join( PACKAGES_DIR, pkg, 'package.json' ) -- ); -- shortToFull.set( pkg, packageJson.name ); -- fullToShort.set( packageJson.name, pkg ); -- fullToPackageJson.set( packageJson.name, packageJson ); -+ for ( const [ pkg, entry ] of PACKAGES ) { -+ shortToFull.set( pkg, entry.packageJson.name ); -+ fullToShort.set( entry.packageJson.name, pkg ); -+ fullToPackageJson.set( entry.packageJson.name, entry.packageJson ); - } - - // Get all routes for dependency tracking -@@ -1918,8 +2068,13 @@ async function watchMode() { - async function rebuildPackage( packageName ) { - try { - const startTime = Date.now(); -+ const entry = PACKAGES.get( packageName ); -+ -+ // External sources are pre-built — only rebundle. -+ if ( ! entry.externalSource ) { -+ await transpilePackage( packageName ); -+ } - -- await transpilePackage( packageName ); - await bundlePackage( packageName ); - - const buildTime = Date.now() - startTime; -@@ -2015,8 +2170,8 @@ async function watchMode() { - await processNextRebuild(); - } - -- const watchPaths = PACKAGES.map( ( packageName ) => -- path.join( PACKAGES_DIR, packageName, 'src' ) -+ const watchPaths = Array.from( PACKAGES.values() ).map( ( entry ) => -+ path.join( entry.dir, 'src' ) - ); - - const watcher = chokidar.watch( watchPaths, { -diff --git a/lib/package-utils.mjs b/lib/package-utils.mjs -index 15b7dd0..1a66afe 100644 ---- a/lib/package-utils.mjs -+++ b/lib/package-utils.mjs -@@ -84,10 +84,39 @@ export function getPackageInfo( fullPackageName, resolveDir = null ) { - return packageJsonCache.get( cacheKey ); - } - -- // Resolve from the package root context to get correct versions - const contextPath = path.join( packageRoot, 'package.json' ); -- const require = createRequire( contextPath ); -- const resolved = require.resolve( `${ fullPackageName }/package.json` ); -+ const localRequire = createRequire( contextPath ); -+ -+ let resolved; -+ try { -+ // Preferred: resolve the package.json subpath directly. -+ resolved = localRequire.resolve( `${ fullPackageName }/package.json` ); -+ } catch { -+ // Fallback for packages whose `exports` field does not expose -+ // `./package.json`. Walk up the directory tree checking each -+ // `node_modules/` — mirrors Node's resolution algorithm without -+ // the exports restriction. -+ let searchDir = packageRoot; -+ const fsRoot = path.parse( searchDir ).root; -+ while ( searchDir !== fsRoot ) { -+ const directPath = path.join( -+ searchDir, -+ 'node_modules', -+ fullPackageName, -+ 'package.json' -+ ); -+ if ( existsSync( directPath ) ) { -+ resolved = directPath; -+ break; -+ } -+ searchDir = path.dirname( searchDir ); -+ } -+ -+ if ( ! resolved ) { -+ return null; -+ } -+ } -+ - const result = getPackageInfoFromFile( resolved ); - packageJsonCache.set( cacheKey, result ); - -diff --git a/lib/wordpress-externals-plugin.mjs b/lib/wordpress-externals-plugin.mjs -index 281749e..04bdc63 100644 ---- a/lib/wordpress-externals-plugin.mjs -+++ b/lib/wordpress-externals-plugin.mjs -@@ -50,13 +50,15 @@ async function generateContentHash( - * @param {string|false} scriptGlobal Global variable name (e.g., 'wp', 'myPlugin') or false to disable globals. - * @param {Object} externalNamespaces Additional namespaces to externalize (e.g., { 'woo': { global: 'woo', handlePrefix: 'woocommerce' } }). - * @param {string} handlePrefix Handle prefix for main package (e.g., 'wp', 'mp'). Defaults to packageNamespace. -+ * @param {Set} [externalPackages] Individual package names to externalize by exact match (e.g., `@acme/shared-ui`). Used for named `packageSources` entries. - * @return {Function} Function that creates the esbuild plugin instance. - */ - export function createWordpressExternalsPlugin( - packageNamespace, - scriptGlobal, - externalNamespaces = {}, -- handlePrefix -+ handlePrefix, -+ externalPackages = new Set() - ) { - /** - * WordPress externals plugin for esbuild. -@@ -277,6 +279,68 @@ export function createWordpressExternalsPlugin( - ); - } - -+ // Handle individual package externals from packageSources. -+ // These match exact package names rather than whole scopes, -+ // avoiding over-broad externalization. -+ for ( const extPkg of externalPackages ) { -+ const escaped = extPkg.replace( -+ /[.*+?^${}()|[\]\\]/g, -+ '\\$&' -+ ); -+ build.onResolve( -+ { filter: new RegExp( `^${ escaped }(/|$)` ) }, -+ /** @param {import('esbuild').OnResolveArgs} args */ -+ ( args ) => { -+ const subpath = -+ args.path.length > extPkg.length -+ ? args.path.slice( extPkg.length + 1 ) -+ : null; -+ -+ const packageJson = getPackageInfo( -+ extPkg, -+ args.resolveDir -+ ); -+ if ( ! packageJson ) { -+ return undefined; -+ } -+ -+ const isScriptModule = isScriptModuleImport( -+ packageJson, -+ subpath -+ ); -+ if ( isScriptModule ) { -+ const kind = -+ args.kind === 'dynamic-import' -+ ? 'dynamic' -+ : 'static'; -+ if ( kind === 'static' ) { -+ moduleDependencies.set( -+ args.path, -+ 'static' -+ ); -+ } else if ( -+ ! moduleDependencies.has( args.path ) -+ ) { -+ moduleDependencies.set( -+ args.path, -+ 'dynamic' -+ ); -+ } -+ -+ return { -+ path: args.path, -+ external: true, -+ sideEffects: !! packageJson.sideEffects, -+ }; -+ } -+ -+ // Not a script module — let esbuild -+ // bundle it inline. -+ return undefined; -+ } -+ ); -+ } -+ - build.onLoad( - { filter: /.*/, namespace: 'vendor-external' }, - /** @param {import('esbuild').OnLoadArgs} args */ diff --git a/.pnpm-patches/@wordpress__build@0.13.0.patch b/.pnpm-patches/@wordpress__build@0.13.0.patch new file mode 100644 index 000000000000..2de61cff8214 --- /dev/null +++ b/.pnpm-patches/@wordpress__build@0.13.0.patch @@ -0,0 +1,702 @@ +Bridge to upstream Gutenberg PR https://github.com/WordPress/gutenberg/pull/78822. + +Decouples script-module identity from `wpPlugin.packageNamespace` (the script-module +ID is read from `package.json#name`) and discovers script-module packages outside +`./packages/` via convention (any entry in `dependencies` that declares +`wpScriptModuleExports`). + +Also extends `getPackageInfo` with a node_modules walk-up fallback so packages whose +`exports` field does not list `./package.json` (e.g. `@automattic/number-formatters`) +are still resolvable. Discovery would silently fall back to inline bundling without +this. This enhancement is part of the same PR proposal; not yet upstream. + +When the upstream PR merges and Jetpack bumps `@wordpress/build` to a version that +includes it, delete this file and remove its entry from `pnpm-workspace.yaml`. + +To rebase this patch to a new `@wordpress/build` version, without a Gutenberg checkout: + + VERSION=0.16.0 + WORK=$(mktemp -d) && cd "$WORK" + curl -sL "https://registry.npmjs.org/@wordpress/build/-/build-$VERSION.tgz" | tar xz + mv package pristine && cp -R pristine patched + curl -sL "https://patch-diff.githubusercontent.com/raw/WordPress/gutenberg/pull/78822.diff" \ + | awk '/^diff --git/{f=($4 ~ /^b\/packages\/wp-build\//)} f' \ + | sed 's|/packages/wp-build/|/|g' \ + | (cd patched && patch -p1 || true) + find patched \( -name '*.orig' -o -name '*.rej' \) -delete + git diff --no-index --no-color pristine patched \ + | sed -E -e 's|^diff --git a/pristine/|diff --git a/|' \ + -e 's| b/patched/| b/|' \ + -e 's|^--- a/pristine/|--- a/|' \ + -e 's|^\+\+\+ b/patched/|+++ b/|' \ + > "$OLDPWD/.pnpm-patches/@wordpress__build@$VERSION.patch" + +Then prepend this header, update pnpm-workspace.yaml, delete the old patch file, +and run `pnpm install` + `pnpm --filter @automattic/jetpack-premium-analytics build` +to validate. + +diff --git a/CHANGELOG.md b/CHANGELOG.md +index eabf9e8..81302b5 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -4,6 +4,11 @@ + + ## 0.13.0 (2026-04-29) + ++### Enhancements ++ ++- Use each package's own `name` field as its script-module ID and externalize internal-package imports by exact name. Decouples script-module identity from `wpPlugin.packageNamespace`, so the npm name survives end-to-end (npm name === import specifier === script-module ID). No-op for Core; enables consumers whose owned npm scope differs from `packageNamespace` to keep a single identifier across npm, IDE, and the WordPress runtime. ++- Discover script-module packages outside `./packages/` via convention. Any entry in the plugin's `dependencies` whose `package.json` declares `wpScriptModuleExports` is registered as a script module, bundled, and externalized under its own npm name. No new config; local packages still take precedence on name collision. ++ + ### Bug Fixes + + - Update the optional `@wordpress/boot`, `@wordpress/route`, and `@wordpress/theme` peer dependency ranges to avoid blocking newer compatible package versions ([#77568](https://github.com/WordPress/gutenberg/pull/77568)). +@@ -20,12 +25,12 @@ + + ### Breaking Changes + +-- `@wordpress/boot`, `@wordpress/route`, `@wordpress/theme`, and `@wordpress/private-apis` are no longer bundled. They are now expected to be provided by WordPress Core (7.0+) or the Gutenberg plugin. ++- `@wordpress/boot`, `@wordpress/route`, `@wordpress/theme`, and `@wordpress/private-apis` are no longer bundled. They are now expected to be provided by WordPress Core (7.0+) or the Gutenberg plugin. + + ### Enhancements + +-- Avoid unexpected results when typecasting `IS_GUTENBERG_PLUGIN` and `IS_WORDPRESS_CORE` values to Booleans ([#75844](https://github.com/WordPress/gutenberg/pull/75844)). +-- Skip PHP transforms during builds when building for WordPress Core ([#75844](https://github.com/WordPress/gutenberg/pull/75844)). ++- Avoid unexpected results when typecasting `IS_GUTENBERG_PLUGIN` and `IS_WORDPRESS_CORE` values to Booleans ([#75844](https://github.com/WordPress/gutenberg/pull/75844)). ++- Skip PHP transforms during builds when building for WordPress Core ([#75844](https://github.com/WordPress/gutenberg/pull/75844)). + + ## 0.9.0 (2026-03-04) + +@@ -33,25 +38,25 @@ + + ## 0.7.0 (2026-01-29) + +-- Update documentation to describe `wpPlugin.name` +-- Add `wpWorkers` field support for automatic worker bundling ([#74785](https://github.com/WordPress/gutenberg/pull/74785)). +-- Add WASM inlining plugin for bundling WebAssembly modules. ++- Update documentation to describe `wpPlugin.name` ++- Add `wpWorkers` field support for automatic worker bundling ([#74785](https://github.com/WordPress/gutenberg/pull/74785)). ++- Add WASM inlining plugin for bundling WebAssembly modules. + + ## 0.6.0 (2026-01-16) + + ### Breaking Changes + +-- Renamed generated PHP files to avoid `index.php` naming conflicts: +- - `build/index.php` → `build/build.php` +- - `build/modules/index.php` → `build/modules/registry.php` +- - `build/scripts/index.php` → `build/scripts/registry.php` +- - `build/styles/index.php` → `build/styles/registry.php` +- - `build/routes/index.php` → `build/routes/registry.php` +-- All generated page functions now include the `{{PREFIX}}` (from `wpPlugin.name`) at the beginning: +- - `register_my_page_route()` → `my_plugin_register_my_page_route()` +- - `my_page_render_page()` → `my_plugin_my_page_render_page()` +- - And similarly for all other page functions +-- Route registration now uses named functions instead of anonymous closures, allowing third-party developers to unhook them ++- Renamed generated PHP files to avoid `index.php` naming conflicts: ++ - `build/index.php` → `build/build.php` ++ - `build/modules/index.php` → `build/modules/registry.php` ++ - `build/scripts/index.php` → `build/scripts/registry.php` ++ - `build/styles/index.php` → `build/styles/registry.php` ++ - `build/routes/index.php` → `build/routes/registry.php` ++- All generated page functions now include the `{{PREFIX}}` (from `wpPlugin.name`) at the beginning: ++ - `register_my_page_route()` → `my_plugin_register_my_page_route()` ++ - `my_page_render_page()` → `my_plugin_my_page_render_page()` ++ - And similarly for all other page functions ++- Route registration now uses named functions instead of anonymous closures, allowing third-party developers to unhook them + + ## 0.4.0 (2025-11-26) + +@@ -61,9 +66,9 @@ + + ### New Features + +-- Initial release of `@wordpress/build` package +-- Transpilation support for TypeScript/JSX to CommonJS and ESM formats +-- SCSS and CSS modules compilation with LTR and RTL support +-- WordPress script and module bundling +-- Automatic PHP registration file generation +-- Watch mode for development ++- Initial release of `@wordpress/build` package ++- Transpilation support for TypeScript/JSX to CommonJS and ESM formats ++- SCSS and CSS modules compilation with LTR and RTL support ++- WordPress script and module bundling ++- Automatic PHP registration file generation ++- Watch mode for development +diff --git a/lib/build.mjs b/lib/build.mjs +index dbf3cac..d11f6db 100755 +--- a/lib/build.mjs ++++ b/lib/build.mjs +@@ -4,8 +4,10 @@ + * External dependencies + */ + import { readFile, writeFile, copyFile, mkdir, unlink } from 'fs/promises'; ++import { existsSync } from 'fs'; + import path from 'path'; + import { createHash } from 'node:crypto'; ++import { createRequire as createNodeRequire } from 'node:module'; + import { parseArgs } from 'node:util'; + import esbuild from 'esbuild'; + import glob from 'fast-glob'; +@@ -89,20 +91,6 @@ const TEST_FILE_PATTERNS = [ + /\.(native|ios|android)\.(js|ts|tsx)$/, + ]; + +-/** +- * Get all package names from the packages directory. +- * +- * @return {string[]} Array of package names. +- */ +-function getAllPackages() { +- return glob +- .sync( normalizePath( path.join( PACKAGES_DIR, '*', 'package.json' ) ) ) +- .map( ( packageJsonPath ) => +- path.basename( path.dirname( packageJsonPath ) ) +- ); +-} +- +-const PACKAGES = getAllPackages(); + const ROOT_PACKAGE_JSON = getPackageInfoFromFile( + path.join( ROOT_DIR, 'package.json' ) + ); +@@ -113,6 +101,116 @@ const HANDLE_PREFIX = WP_PLUGIN_CONFIG.handlePrefix || PACKAGE_NAMESPACE; + const EXTERNAL_NAMESPACES = WP_PLUGIN_CONFIG.externalNamespaces || {}; + const PAGES = WP_PLUGIN_CONFIG.pages || []; + ++/** ++ * A discovered package in the registry. ++ * ++ * @typedef {Object} PackageEntry ++ * @property {string} dir Absolute path to the package directory. ++ * @property {import('./package-utils.mjs').PackageJson} packageJson Parsed package.json contents. ++ * @property {boolean} external True when the package is pre-built outside this plugin (e.g. a workspace dep). External packages are bundled and externalized but not transpiled; local packages from `./packages/` are also transpiled from source. ++ */ ++ ++/** ++ * Build the registry of script-module packages this plugin builds. ++ * ++ * Two convention-driven discovery sources: ++ * ++ * 1. Local packages: every `./packages//package.json`. ++ * 2. Convention deps: every entry in the plugin's `dependencies` whose own ++ * `package.json` declares `wpScriptModuleExports`. Lets a plugin pull in ++ * shared script-module packages from outside `./packages/` (workspace ++ * siblings, npm-installed siblings) without any extra wp-build config. ++ * Local packages win first-match for any given name. ++ * ++ * @return {Map} Registry keyed by short identifier: ++ * directory name for local packages, full npm name for convention deps. ++ */ ++function getAllPackages() { ++ const registry = new Map(); ++ ++ // 1. Local packages from ./packages/* ++ const localPaths = glob.sync( ++ normalizePath( path.join( PACKAGES_DIR, '*', 'package.json' ) ) ++ ); ++ for ( const pkgJsonPath of localPaths ) { ++ const dir = path.dirname( pkgJsonPath ); ++ const key = path.basename( dir ); ++ registry.set( key, { ++ dir, ++ packageJson: getPackageInfoFromFile( pkgJsonPath ), ++ external: false, ++ } ); ++ } ++ ++ // 2. Convention deps with wpScriptModuleExports ++ const deps = Object.keys( ROOT_PACKAGE_JSON.dependencies || {} ); ++ const localNames = new Set( ++ Array.from( registry.values() ).map( ++ ( entry ) => entry.packageJson.name ++ ) ++ ); ++ const localRequire = createNodeRequire( ++ path.join( ROOT_DIR, 'package.json' ) ++ ); ++ for ( const depName of deps ) { ++ // First-match-wins: a local package with the same `name` already ++ // claimed this slot, skip the dep. ++ if ( localNames.has( depName ) || registry.has( depName ) ) { ++ continue; ++ } ++ ++ // Resolve the dep's package.json. Some packages don't expose it in ++ // their `exports`, so fall back to a direct node_modules lookup. ++ let pkgJsonPath; ++ try { ++ pkgJsonPath = localRequire.resolve( `${ depName }/package.json` ); ++ } catch { ++ const direct = path.join( ++ ROOT_DIR, ++ 'node_modules', ++ depName, ++ 'package.json' ++ ); ++ if ( ! existsSync( direct ) ) { ++ continue; ++ } ++ pkgJsonPath = direct; ++ } ++ ++ const depPackageJson = getPackageInfoFromFile( pkgJsonPath ); ++ if ( ! depPackageJson.wpScriptModuleExports ) { ++ continue; ++ } ++ ++ if ( depPackageJson.wpScript ) { ++ console.warn( ++ `wp-build: ${ depName } declares wpScript; ignored (external dependencies contribute script modules only).` ++ ); ++ } ++ ++ registry.set( depName, { ++ dir: path.dirname( pkgJsonPath ), ++ packageJson: depPackageJson, ++ external: true, ++ } ); ++ } ++ ++ return registry; ++} ++ ++const PACKAGES = getAllPackages(); ++ ++// Set of every discovered package's `name` field. Used by the externals ++// plugin to externalize internal-package imports by exact name, regardless ++// of `packageNamespace`. Decouples script-module identity from a config ++// string so a package's own `name` survives end-to-end (npm name === import ++// specifier === script-module ID). ++const INTERNAL_PACKAGE_NAMES = new Set( ++ Array.from( PACKAGES.values() ) ++ .map( ( entry ) => entry.packageJson.name ) ++ .filter( Boolean ) ++); ++ + /** + * Interprets a configuration value as a boolean, where `"true"` and `"1"` + * are considered true while all other values are false. +@@ -153,7 +251,8 @@ const wordpressExternalsPlugin = createWordpressExternalsPlugin( + PACKAGE_NAMESPACE, + SCRIPT_GLOBAL, + EXTERNAL_NAMESPACES, +- HANDLE_PREFIX ++ HANDLE_PREFIX, ++ INTERNAL_PACKAGE_NAMES + ); + + /** +@@ -480,23 +579,25 @@ function resolveEntryPoint( packageDir, packageJson ) { + */ + async function bundlePackage( packageName, options = {} ) { + const { +- sourceDir = PACKAGES_DIR, + handlePrefix = HANDLE_PREFIX, + scriptGlobal = SCRIPT_GLOBAL, + packageNamespace = PACKAGE_NAMESPACE, + } = options; + ++ const entry = PACKAGES.get( packageName ); ++ const packageDir = entry.dir; ++ const packageJson = entry.packageJson; ++ + const builtModules = []; + const builtScripts = []; + const builtStyles = []; +- const packageDir = path.join( sourceDir, packageName ); +- const packageJson = getPackageInfoFromFile( +- path.join( sourceDir, packageName, 'package.json' ) +- ); + + const builds = []; + +- if ( packageJson.wpScript ) { ++ // External (convention-discovered) packages contribute script modules only. ++ const buildAsScript = !! packageJson.wpScript && ! entry.external; ++ ++ if ( buildAsScript ) { + const entryPoint = resolveEntryPoint( packageDir, packageJson ); + const outputDir = path.join( BUILD_DIR, 'scripts', packageName ); + const target = browserslistToEsbuild(); +@@ -653,10 +754,16 @@ async function bundlePackage( packageName, options = {} ) { + ); + } + ++ // The script-module ID is the package's own `name` field. The ++ // PHP registry, the asset manifest, and `wp_register_script_module` ++ // all treat the ID as an opaque string, so this lets the npm name ++ // survive end-to-end without being rewritten by build configuration. ++ // Falls back to the legacy `@/` shape ++ // only when `name` is missing (e.g. an unnamed local package). ++ const packageId = ++ packageJson.name || `@${ packageNamespace }/${ packageName }`; + const scriptModuleId = +- exportName === '.' +- ? `@${ packageNamespace }/${ packageName }` +- : `@${ packageNamespace }/${ packageName }/${ fileName }`; ++ exportName === '.' ? packageId : `${ packageId }/${ fileName }`; + + builtModules.push( { + id: scriptModuleId, +@@ -668,7 +775,7 @@ async function bundlePackage( packageName, options = {} ) { + } + + let hasMainStyle = false; +- if ( packageJson.wpScript ) { ++ if ( buildAsScript ) { + const buildStyleDir = path.join( packageDir, 'build-style' ); + const outputDir = path.join( BUILD_DIR, 'styles', packageName ); + +@@ -862,7 +969,7 @@ async function inferStyleDependencies( scriptDependencies, packageName ) { + + const styleDeps = []; + // Get the resolve directory for context-aware package resolution +- const resolveDir = path.join( PACKAGES_DIR, packageName ); ++ const resolveDir = PACKAGES.get( packageName )?.dir || PACKAGES_DIR; + + for ( const scriptHandle of scriptDependencies ) { + // Skip non-package dependencies (like 'react', 'lodash', etc.) +@@ -1215,17 +1322,17 @@ async function generatePagesPhp( pageData, replacements ) { + */ + async function transpilePackage( packageName ) { + const startTime = Date.now(); +- const packageDir = path.join( PACKAGES_DIR, packageName ); +- const packageJson = getPackageInfoFromFile( +- path.join( PACKAGES_DIR, packageName, 'package.json' ) +- ); ++ const entry = PACKAGES.get( packageName ); + +- if ( ! packageJson ) { ++ if ( ! entry ) { + throw new Error( + `Could not find package.json for package: ${ packageName }` + ); + } + ++ const packageDir = entry.dir; ++ const packageJson = entry.packageJson; ++ + const srcFiles = await glob( `src/**/*.${ SOURCE_EXTENSIONS }`, { + cwd: packageDir, + ignore: IGNORE_PATTERNS, +@@ -1432,10 +1539,9 @@ async function transpilePackage( packageName ) { + * @return {Promise} Build time in milliseconds, or null if no styles. + */ + async function compileStyles( packageName ) { +- const packageDir = path.join( PACKAGES_DIR, packageName ); +- const packageJson = getPackageInfoFromFile( +- path.join( PACKAGES_DIR, packageName, 'package.json' ) +- ); ++ const entry = PACKAGES.get( packageName ); ++ const packageDir = entry.dir; ++ const packageJson = entry.packageJson; + + // Get SCSS entry point patterns from package.json, default to root-level only + const scssEntryPointPatterns = packageJson.wpStyleEntryPoints || [ +@@ -1543,12 +1649,20 @@ function isPackageSourceFile( filename ) { + return false; + } + +- return PACKAGES.some( ( packageName ) => { ++ for ( const entry of PACKAGES.values() ) { ++ // External packages are not transpiled from source, so their files ++ // do not trigger rebuilds via this path. ++ if ( entry.external ) { ++ continue; ++ } + const packagePath = normalizePath( +- path.join( 'packages', packageName ) ++ path.relative( ROOT_DIR, entry.dir ) + ); +- return relativePath.startsWith( packagePath + '/' ); +- } ); ++ if ( relativePath.startsWith( packagePath + '/' ) ) { ++ return true; ++ } ++ } ++ return false; + } + + /** +@@ -1562,9 +1676,12 @@ function getPackageName( filename ) { + path.relative( process.cwd(), filename ) + ); + +- for ( const packageName of PACKAGES ) { ++ for ( const [ packageName, entry ] of PACKAGES ) { ++ if ( entry.external ) { ++ continue; ++ } + const packagePath = normalizePath( +- path.join( 'packages', packageName ) ++ path.relative( ROOT_DIR, entry.dir ) + ); + if ( relativePath.startsWith( packagePath + '/' ) ) { + return packageName; +@@ -1727,17 +1844,14 @@ async function buildAll( baseUrlExpression ) { + + const startTime = Date.now(); + +- // Build maps: short name ↔ full name ↔ package.json from package.json files ++ // Build maps: short name ↔ full name ↔ package.json from the registry. + const shortToFull = new Map(); + const fullToShort = new Map(); + const fullToPackageJson = new Map(); +- for ( const pkg of PACKAGES ) { +- const packageJson = getPackageInfoFromFile( +- path.join( PACKAGES_DIR, pkg, 'package.json' ) +- ); +- shortToFull.set( pkg, packageJson.name ); +- fullToShort.set( packageJson.name, pkg ); +- fullToPackageJson.set( packageJson.name, packageJson ); ++ for ( const [ pkg, entry ] of PACKAGES ) { ++ shortToFull.set( pkg, entry.packageJson.name ); ++ fullToShort.set( entry.packageJson.name, pkg ); ++ fullToPackageJson.set( entry.packageJson.name, entry.packageJson ); + } + + const levels = groupByDepth( fullToPackageJson ); +@@ -1748,6 +1862,15 @@ async function buildAll( baseUrlExpression ) { + await Promise.all( + level.map( async ( fullName ) => { + const packageName = fullToShort.get( fullName ); ++ const entry = PACKAGES.get( packageName ); ++ ++ // External packages are pre-built outside this plugin ++ // (e.g. a workspace dep). Skip transpilation; they are ++ // bundled and externalized in Phase 2. ++ if ( entry.external ) { ++ return; ++ } ++ + const buildTime = await transpilePackage( packageName ); + console.log( + ` ✔ Transpiled ${ packageName } (${ buildTime }ms)` +@@ -1761,7 +1884,7 @@ async function buildAll( baseUrlExpression ) { + const scripts = []; + const styles = []; + await Promise.all( +- PACKAGES.map( async ( packageName ) => { ++ Array.from( PACKAGES.keys() ).map( async ( packageName ) => { + const startBundleTime = Date.now(); + const ret = await bundlePackage( packageName ); + const buildTime = Date.now() - startBundleTime; +@@ -1894,17 +2017,14 @@ async function watchMode() { + let isRebuilding = false; + const needsRebuild = new Set(); + +- // Build maps: short name ↔ full name ↔ package.json from package.json files (once) ++ // Build maps: short name ↔ full name ↔ package.json from the registry (once) + const shortToFull = new Map(); + const fullToShort = new Map(); + const fullToPackageJson = new Map(); +- for ( const pkg of PACKAGES ) { +- const packageJson = getPackageInfoFromFile( +- path.join( PACKAGES_DIR, pkg, 'package.json' ) +- ); +- shortToFull.set( pkg, packageJson.name ); +- fullToShort.set( packageJson.name, pkg ); +- fullToPackageJson.set( packageJson.name, packageJson ); ++ for ( const [ pkg, entry ] of PACKAGES ) { ++ shortToFull.set( pkg, entry.packageJson.name ); ++ fullToShort.set( entry.packageJson.name, pkg ); ++ fullToPackageJson.set( entry.packageJson.name, entry.packageJson ); + } + + // Get all routes for dependency tracking +@@ -1918,8 +2038,14 @@ async function watchMode() { + async function rebuildPackage( packageName ) { + try { + const startTime = Date.now(); ++ const entry = PACKAGES.get( packageName ); + +- await transpilePackage( packageName ); ++ // External packages are pre-built outside this plugin; only ++ // rebundle them when their declared script-module entry is ++ // regenerated. Skip transpilation. ++ if ( ! entry?.external ) { ++ await transpilePackage( packageName ); ++ } + await bundlePackage( packageName ); + + const buildTime = Date.now() - startTime; +@@ -2015,9 +2141,9 @@ async function watchMode() { + await processNextRebuild(); + } + +- const watchPaths = PACKAGES.map( ( packageName ) => +- path.join( PACKAGES_DIR, packageName, 'src' ) +- ); ++ const watchPaths = Array.from( PACKAGES.values() ) ++ .filter( ( entry ) => ! entry.external ) ++ .map( ( entry ) => path.join( entry.dir, 'src' ) ); + + const watcher = chokidar.watch( watchPaths, { + ignored: [ +diff --git a/lib/package-utils.mjs b/lib/package-utils.mjs +index 15b7dd0..be4daa7 100644 +--- a/lib/package-utils.mjs ++++ b/lib/package-utils.mjs +@@ -87,7 +87,48 @@ export function getPackageInfo( fullPackageName, resolveDir = null ) { + // Resolve from the package root context to get correct versions + const contextPath = path.join( packageRoot, 'package.json' ); + const require = createRequire( contextPath ); +- const resolved = require.resolve( `${ fullPackageName }/package.json` ); ++ ++ let resolved; ++ try { ++ // Preferred path: the package exposes ./package.json via `exports`. ++ resolved = require.resolve( `${ fullPackageName }/package.json` ); ++ } catch ( error ) { ++ const code = /** @type {NodeJS.ErrnoException} */ ( error ).code; ++ if ( ++ code !== 'MODULE_NOT_FOUND' && ++ code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' ++ ) { ++ throw error; ++ } ++ ++ // Fallback: walk up node_modules and look for the package directly. ++ // Covers two real cases: ++ // - Packages whose `exports` field does not list `./package.json`. ++ // - Imports resolved by other means (tsconfig paths, esbuild aliases) ++ // where the package is genuinely not in node_modules; the walk-up ++ // finds nothing and we return `null` so callers fall through. ++ let searchDir = packageRoot; ++ const fsRoot = path.parse( searchDir ).root; ++ while ( searchDir !== fsRoot ) { ++ const directPath = path.join( ++ searchDir, ++ 'node_modules', ++ fullPackageName, ++ 'package.json' ++ ); ++ if ( existsSync( directPath ) ) { ++ resolved = directPath; ++ break; ++ } ++ searchDir = path.dirname( searchDir ); ++ } ++ ++ if ( ! resolved ) { ++ packageJsonCache.set( cacheKey, null ); ++ return null; ++ } ++ } ++ + const result = getPackageInfoFromFile( resolved ); + packageJsonCache.set( cacheKey, result ); + +diff --git a/lib/wordpress-externals-plugin.mjs b/lib/wordpress-externals-plugin.mjs +index 2b7e01a..a07a310 100644 +--- a/lib/wordpress-externals-plugin.mjs ++++ b/lib/wordpress-externals-plugin.mjs +@@ -46,17 +46,19 @@ async function generateContentHash( + * This plugin handles WordPress package externals and vendor libraries, + * treating them as external dependencies available via global variables. + * +- * @param {string} packageNamespace Custom package namespace (e.g., 'wordpress', 'my-plugin'). +- * @param {string|false} scriptGlobal Global variable name (e.g., 'wp', 'myPlugin') or false to disable globals. +- * @param {Object} externalNamespaces Additional namespaces to externalize (e.g., { 'woo': { global: 'woo', handlePrefix: 'woocommerce' } }). +- * @param {string} handlePrefix Handle prefix for main package (e.g., 'wp', 'mp'). Defaults to packageNamespace. ++ * @param {string} packageNamespace Custom package namespace (e.g., 'wordpress', 'my-plugin'). ++ * @param {string|false} scriptGlobal Global variable name (e.g., 'wp', 'myPlugin') or false to disable globals. ++ * @param {Object} externalNamespaces Additional namespaces to externalize (e.g., { 'woo': { global: 'woo', handlePrefix: 'woocommerce' } }). ++ * @param {string} handlePrefix Handle prefix for main package (e.g., 'wp', 'mp'). Defaults to packageNamespace. ++ * @param {Set} [internalPackageNames] `name` fields of every internal package (local + convention-discovered). Imports matching any of these are externalized by exact name, regardless of `packageNamespace`. + * @return {Function} Function that creates the esbuild plugin instance. + */ + export function createWordpressExternalsPlugin( + packageNamespace, + scriptGlobal, + externalNamespaces = {}, +- handlePrefix ++ handlePrefix, ++ internalPackageNames = new Set() + ) { + /** + * WordPress externals plugin for esbuild. +@@ -199,6 +201,83 @@ export function createWordpressExternalsPlugin( + ); + } + ++ // Externalize internal packages by exact name. Must precede the ++ // namespace pattern below so internal names win over the wildcard. ++ if ( internalPackageNames.size > 0 ) { ++ // Longest-first: avoids `@org/block` shadowing `@org/block-editor`. ++ const namesSorted = Array.from( internalPackageNames ).sort( ++ ( a, b ) => b.length - a.length ++ ); ++ const escapedNames = namesSorted.map( ( n ) => ++ n.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ) ++ ); ++ const internalNamesFilter = new RegExp( ++ `^(?:${ escapedNames.join( '|' ) })(?:/|$)` ++ ); ++ ++ build.onResolve( ++ { filter: internalNamesFilter }, ++ /** @param {import('esbuild').OnResolveArgs} args */ ++ ( args ) => { ++ const head = args.path.startsWith( '@' ) ++ ? args.path.split( '/', 2 ).join( '/' ) ++ : args.path.split( '/', 1 )[ 0 ]; ++ ++ if ( ! internalPackageNames.has( head ) ) { ++ return undefined; ++ } ++ ++ const subpath = ++ args.path.length > head.length ++ ? args.path.slice( head.length + 1 ) ++ : null; ++ ++ const packageJson = getPackageInfo( ++ head, ++ args.resolveDir ++ ); ++ ++ if ( ! packageJson ) { ++ return undefined; ++ } ++ ++ const isScriptModule = isScriptModuleImport( ++ packageJson, ++ subpath ++ ); ++ const isScript = !! packageJson.wpScript; ++ ++ // Dual packages: IIFE yields to the namespace handler. ++ let externalize = isScriptModule; ++ if ( isScriptModule && isScript ) { ++ externalize = buildFormat === 'esm'; ++ } ++ if ( ! externalize ) { ++ return undefined; ++ } ++ ++ const kind = ++ args.kind === 'dynamic-import' ++ ? 'dynamic' ++ : 'static'; ++ ++ if ( kind === 'static' ) { ++ moduleDependencies.set( args.path, 'static' ); ++ } else if ( ++ ! moduleDependencies.has( args.path ) ++ ) { ++ moduleDependencies.set( args.path, 'dynamic' ); ++ } ++ ++ return { ++ path: args.path, ++ external: true, ++ sideEffects: !! packageJson.sideEffects, ++ }; ++ } ++ ); ++ } ++ + // Handle package namespace externals (wordpress and custom) + for ( const externalConfig of packageExternals ) { + build.onResolve( diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ce74003ce3d7..92c1b00598c0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -79,9 +79,10 @@ ignoredOptionalDependencies: # Dependencies needing patching. patchedDependencies: - # Add wpPlugin.packageSources for cross-directory package discovery. - # Upstream PR: https://github.com/WordPress/gutenberg/pull/77226 - '@wordpress/build@0.12.0': .pnpm-patches/@wordpress__build@0.12.0.patch + # Decouple script-module identity from `wpPlugin.packageNamespace` and + # discover script-module packages outside `./packages/` via convention. + # Upstream PR: https://github.com/WordPress/gutenberg/pull/78822 + '@wordpress/build@0.13.0': .pnpm-patches/@wordpress__build@0.13.0.patch # Vite/esbuild doesn't like the `__esModule` + `exports["default"]` pattern, it winds up double-wrapping the default. # See also https://github.com/WordPress/gutenberg/issues/39619 diff --git a/projects/packages/premium-analytics/changelog/update-wp-build-package-sources b/projects/packages/premium-analytics/changelog/update-wp-build-package-sources index 2bd3c476d8cb..6830603f86cf 100644 --- a/projects/packages/premium-analytics/changelog/update-wp-build-package-sources +++ b/projects/packages/premium-analytics/changelog/update-wp-build-package-sources @@ -1,4 +1,4 @@ Significance: patch Type: changed -Add packageSources support and number-formatters integration. +Patch `@wordpress/build` to read script-module IDs from `package.json#name` and discover script-module packages outside `./packages/` via convention. Pulls in `@automattic/number-formatters` as the first cross-directory shared script module. From e6cef49cd08b1d24dc9e6c563bd3f9f32ad090b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 1 Jun 2026 19:09:28 +0100 Subject: [PATCH 5/8] scope wp-build patch deregister to @wordpress/* mirror upstream PR #77465 so shared non-core modules use Core first-wins --- .pnpm-patches/@wordpress__build@0.13.0.patch | 70 ++++++++++++++++++-- pnpm-lock.yaml | 28 ++++---- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/.pnpm-patches/@wordpress__build@0.13.0.patch b/.pnpm-patches/@wordpress__build@0.13.0.patch index 2de61cff8214..1447609326f4 100644 --- a/.pnpm-patches/@wordpress__build@0.13.0.patch +++ b/.pnpm-patches/@wordpress__build@0.13.0.patch @@ -1,10 +1,17 @@ -Bridge to upstream Gutenberg PR https://github.com/WordPress/gutenberg/pull/78822. +Bridge to upstream Gutenberg PRs https://github.com/WordPress/gutenberg/pull/78822 +and https://github.com/WordPress/gutenberg/pull/77465. Decouples script-module identity from `wpPlugin.packageNamespace` (the script-module ID is read from `package.json#name`) and discovers script-module packages outside `./packages/` via convention (any entry in `dependencies` that declares `wpScriptModuleExports`). +Scopes the generated PHP `wp_deregister_script_module()` calls to `@wordpress/*` IDs +only (PR #77465). Shared non-core packages registered by more than one wp-build +plugin then get Core's idempotent first-wins semantics instead of last-plugin-wins +collisions. Without this, the shared identity from convention discovery above would +re-introduce the cross-plugin version-skew concern. + Also extends `getPackageInfo` with a node_modules walk-up fallback so packages whose `exports` field does not list `./package.json` (e.g. `@automattic/number-formatters`) are still resolvable. Discovery would silently fall back to inline bundling without @@ -39,14 +46,15 @@ diff --git a/CHANGELOG.md b/CHANGELOG.md index eabf9e8..81302b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md -@@ -4,6 +4,11 @@ - +@@ -4,6 +4,12 @@ + ## 0.13.0 (2026-04-29) - + +### Enhancements + +- Use each package's own `name` field as its script-module ID and externalize internal-package imports by exact name. Decouples script-module identity from `wpPlugin.packageNamespace`, so the npm name survives end-to-end (npm name === import specifier === script-module ID). No-op for Core; enables consumers whose owned npm scope differs from `packageNamespace` to keep a single identifier across npm, IDE, and the WordPress runtime. +- Discover script-module packages outside `./packages/` via convention. Any entry in the plugin's `dependencies` whose `package.json` declares `wpScriptModuleExports` is registered as a script module, bundled, and externalized under its own npm name. No new config; local packages still take precedence on name collision. ++- Scope the generated `wp_deregister_script_module()` calls to `@wordpress/*` IDs (PR #77465), so shared non-core packages registered by multiple wp-build plugins fall through to Core's idempotent first-wins semantics instead of last-plugin-wins collisions. + ### Bug Fixes @@ -700,3 +708,57 @@ index 2b7e01a..a07a310 100644 // Handle package namespace externals (wordpress and custom) for ( const externalConfig of packageExternals ) { build.onResolve( +diff --git a/templates/module-registration.php.template b/templates/module-registration.php.template +index d2c8712273..e86be5f324 100644 +--- a/templates/module-registration.php.template ++++ b/templates/module-registration.php.template +@@ -42,8 +42,13 @@ function {{PREFIX}}_register_script_modules() { + $asset = file_exists( $asset_path ) ? require $asset_path : array(); + + // Deregister first to override any previously registered version +- // (e.g., Core's default modules when running as a plugin). +- wp_deregister_script_module( $module['id'] ); ++ // (e.g., Core's default modules when running as a plugin). Scoped to ++ // `@wordpress/*`, the only namespace Core registers by default, so shared ++ // non-core modules fall through to Core's idempotent first-wins semantics ++ // instead of silent last-plugin-wins collisions across wp-build plugins. ++ if ( str_starts_with( $module['id'], '@wordpress/' ) ) { ++ wp_deregister_script_module( $module['id'] ); ++ } + + wp_register_script_module( + $module['id'], +diff --git a/templates/routes-registration.php.template b/templates/routes-registration.php.template +index 7ab43f1eb1..8aeaf29d0f 100644 +--- a/templates/routes-registration.php.template ++++ b/templates/routes-registration.php.template +@@ -54,8 +54,12 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name + $content_handle = '{{HANDLE_PREFIX}}/routes/' . $route['name'] . '/content'; + $extension = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.js' : '.min.js'; + // Deregister first to override any previously registered version +- // (e.g., Core's default modules when running as a plugin). +- wp_deregister_script_module( $content_handle ); ++ // (e.g., Core's default modules). Scoped to `@wordpress/*` so ++ // plugin-local route modules fall through to Core's idempotent ++ // first-wins semantics instead of last-plugin-wins collisions. ++ if ( str_starts_with( $content_handle, '@wordpress/' ) ) { ++ wp_deregister_script_module( $content_handle ); ++ } + wp_register_script_module( + $content_handle, + $build_constants['build_url'] . 'routes/' . $route['name'] . '/content' . $extension, +@@ -73,8 +77,12 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name + $route_handle = '{{HANDLE_PREFIX}}/routes/' . $route['name'] . '/route'; + $extension = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.js' : '.min.js'; + // Deregister first to override any previously registered version +- // (e.g., Core's default modules when running as a plugin). +- wp_deregister_script_module( $route_handle ); ++ // (e.g., Core's default modules). Scoped to `@wordpress/*` so ++ // plugin-local route modules fall through to Core's idempotent ++ // first-wins semantics instead of last-plugin-wins collisions. ++ if ( str_starts_with( $route_handle, '@wordpress/' ) ) { ++ wp_deregister_script_module( $route_handle ); ++ } + wp_register_script_module( + $route_handle, + $build_constants['build_url'] . 'routes/' . $route['name'] . '/route' . $extension, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4625eb109068..8e88b0bcde44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,7 @@ pnpmfileChecksum: sha256-KrBHPLv8FlPoa5OUGbvotku6TLmuhZiq22jdIe7yBHc= patchedDependencies: '@wordpress/build@0.13.0': - hash: c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5 + hash: fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f path: .pnpm-patches/@wordpress__build@0.13.0.patch react-autosize-textarea: hash: 5c09e1dee59caaaba3871f9d722f93e56b41169db486b059597e8f8c788aa464 @@ -2126,7 +2126,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: ^4.24.0 version: 4.28.2 @@ -2741,7 +2741,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) '@wordpress/date': specifier: 5.46.0 version: 5.46.0 @@ -3449,7 +3449,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: ^4.24.0 version: 4.28.2 @@ -3790,7 +3790,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) '@wordpress/theme': specifier: 0.13.0 version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3863,7 +3863,7 @@ importers: version: 18.3.28 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: 4.28.2 version: 4.28.2 @@ -4069,7 +4069,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) autoprefixer: specifier: 10.4.20 version: 10.4.20(postcss@8.5.14) @@ -4296,7 +4296,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: ^4.24.0 version: 4.28.2 @@ -4719,7 +4719,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) autoprefixer: specifier: 10.4.20 version: 10.4.20(postcss@8.5.14) @@ -4792,7 +4792,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) '@wordpress/icons': specifier: ^13.0.0 version: 13.1.0(react@18.3.1) @@ -24115,7 +24115,7 @@ snapshots: '@wordpress/browserslist-config@6.46.0': {} - '@wordpress/build@0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.5.0(postcss@8.5.14) @@ -24141,7 +24141,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.5.0(postcss@8.5.14) @@ -24166,7 +24166,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.5.0(postcss@8.5.14) @@ -24193,7 +24193,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.13.0(patch_hash=c6145464ae34a4a023cc350d4948b934922600f5a04c876780c79b3f51882cb5)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.5.0(postcss@8.5.14) From f4f189a01ecfb35f95413d352c3fbf3b42a2a66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 1 Jun 2026 19:09:32 +0100 Subject: [PATCH 6/8] migrate premium-analytics init to npm name rename init to @automattic/jetpack-premium-analytics-init; align pages[].init so name, specifier, and module ID match --- .../premium-analytics/changelog/update-wp-build-package-sources | 2 +- projects/packages/premium-analytics/package.json | 2 +- projects/packages/premium-analytics/packages/init/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/packages/premium-analytics/changelog/update-wp-build-package-sources b/projects/packages/premium-analytics/changelog/update-wp-build-package-sources index 6830603f86cf..f690d6bc7e56 100644 --- a/projects/packages/premium-analytics/changelog/update-wp-build-package-sources +++ b/projects/packages/premium-analytics/changelog/update-wp-build-package-sources @@ -1,4 +1,4 @@ Significance: patch Type: changed -Patch `@wordpress/build` to read script-module IDs from `package.json#name` and discover script-module packages outside `./packages/` via convention. Pulls in `@automattic/number-formatters` as the first cross-directory shared script module. +Patch `@wordpress/build` to read script-module IDs from `package.json#name`, discover script-module packages outside `./packages/` via convention, and scope the generated `wp_deregister_script_module()` calls to `@wordpress/*` so shared modules fall through to Core's first-wins. Pulls in `@automattic/number-formatters` as the first cross-directory shared script module, and renames the local `init` package to `@automattic/jetpack-premium-analytics-init` so its npm name, import specifier, and script-module ID all match. diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index 31814c2cc467..ca075b69a084 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -16,7 +16,7 @@ { "id": "jetpack-premium-analytics", "init": [ - "@jetpack-premium-analytics/init" + "@automattic/jetpack-premium-analytics-init" ] } ], diff --git a/projects/packages/premium-analytics/packages/init/package.json b/projects/packages/premium-analytics/packages/init/package.json index 6ee2ce1e4470..70de0aea9d5b 100644 --- a/projects/packages/premium-analytics/packages/init/package.json +++ b/projects/packages/premium-analytics/packages/init/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "_@jetpack-premium-analytics/init", + "name": "@automattic/jetpack-premium-analytics-init", "version": "0.1.0", "type": "module", "wpScript": true, From a4000ad0df8a45b16db319fe905bc6c89ce23968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 1 Jun 2026 21:19:12 +0100 Subject: [PATCH 7/8] make wp-build patch faithfully mirror upstream regenerate from #78822 + #77465 diffs: drop the node_modules walk-up (number-formatters now exposes ./package.json) and adopt #77465's exact template comments --- .pnpm-patches/@wordpress__build@0.13.0.patch | 219 ++++--------------- pnpm-lock.yaml | 28 +-- 2 files changed, 61 insertions(+), 186 deletions(-) diff --git a/.pnpm-patches/@wordpress__build@0.13.0.patch b/.pnpm-patches/@wordpress__build@0.13.0.patch index 1447609326f4..8b4a5bf6f62b 100644 --- a/.pnpm-patches/@wordpress__build@0.13.0.patch +++ b/.pnpm-patches/@wordpress__build@0.13.0.patch @@ -1,24 +1,23 @@ Bridge to upstream Gutenberg PRs https://github.com/WordPress/gutenberg/pull/78822 -and https://github.com/WordPress/gutenberg/pull/77465. +and https://github.com/WordPress/gutenberg/pull/77465. This patch is exactly those two +PRs applied to `@wordpress/build@0.13.0`; the only addition is a one-line +`createNodeRequire` import that 0.13.0 predates (the PR branch sits on newer trunk where +it already exists). -Decouples script-module identity from `wpPlugin.packageNamespace` (the script-module -ID is read from `package.json#name`) and discovers script-module packages outside -`./packages/` via convention (any entry in `dependencies` that declares -`wpScriptModuleExports`). +#78822 reads each script module's ID from `package.json#name`, externalizes +internal-package imports by exact name, and discovers script-module packages outside +`./packages/` via convention (any `dependencies` entry whose own `package.json` declares +`wpScriptModuleExports`). #77465 scopes the generated PHP `wp_deregister_script_module()` +calls to `@wordpress/*` IDs, so shared non-core packages registered by more than one +wp-build plugin get Core's idempotent first-wins semantics instead of last-plugin-wins. -Scopes the generated PHP `wp_deregister_script_module()` calls to `@wordpress/*` IDs -only (PR #77465). Shared non-core packages registered by more than one wp-build -plugin then get Core's idempotent first-wins semantics instead of last-plugin-wins -collisions. Without this, the shared identity from convention discovery above would -re-introduce the cross-plugin version-skew concern. +Convention discovery reads each shared package's `package.json` through its `exports` +field, so a discovered package must expose `./package.json` (e.g. +`@automattic/number-formatters` adds `"./package.json": "./package.json"`). Packages +that do not are skipped and bundled inline. -Also extends `getPackageInfo` with a node_modules walk-up fallback so packages whose -`exports` field does not list `./package.json` (e.g. `@automattic/number-formatters`) -are still resolvable. Discovery would silently fall back to inline bundling without -this. This enhancement is part of the same PR proposal; not yet upstream. - -When the upstream PR merges and Jetpack bumps `@wordpress/build` to a version that -includes it, delete this file and remove its entry from `pnpm-workspace.yaml`. +When the upstream PRs merge and Jetpack bumps `@wordpress/build` to a version that +includes them, delete this file and remove its entry from `pnpm-workspace.yaml`. To rebase this patch to a new `@wordpress/build` version, without a Gutenberg checkout: @@ -26,11 +25,16 @@ To rebase this patch to a new `@wordpress/build` version, without a Gutenberg ch WORK=$(mktemp -d) && cd "$WORK" curl -sL "https://registry.npmjs.org/@wordpress/build/-/build-$VERSION.tgz" | tar xz mv package pristine && cp -R pristine patched - curl -sL "https://patch-diff.githubusercontent.com/raw/WordPress/gutenberg/pull/78822.diff" \ - | awk '/^diff --git/{f=($4 ~ /^b\/packages\/wp-build\//)} f' \ - | sed 's|/packages/wp-build/|/|g' \ - | (cd patched && patch -p1 || true) + for PR in 78822 77465; do + curl -sL "https://patch-diff.githubusercontent.com/raw/WordPress/gutenberg/pull/$PR.diff" \ + | awk '/^diff --git/{f=($4 ~ /^b\/packages\/wp-build\//)} f' \ + | sed 's|/packages/wp-build/|/|g' \ + | (cd patched && patch -p1 || true) + done find patched \( -name '*.orig' -o -name '*.rej' \) -delete + # 0.13.0 predates this import; newer versions may already include it. + grep -q "createRequire as createNodeRequire" patched/lib/build.mjs || \ + perl -0pi -e "s/(import \{ createHash \} from 'node:crypto';\n)/\$1import { createRequire as createNodeRequire } from 'node:module';\n/" patched/lib/build.mjs git diff --no-index --no-color pristine patched \ | sed -E -e 's|^diff --git a/pristine/|diff --git a/|' \ -e 's| b/patched/| b/|' \ @@ -38,101 +42,29 @@ To rebase this patch to a new `@wordpress/build` version, without a Gutenberg ch -e 's|^\+\+\+ b/patched/|+++ b/|' \ > "$OLDPWD/.pnpm-patches/@wordpress__build@$VERSION.patch" -Then prepend this header, update pnpm-workspace.yaml, delete the old patch file, -and run `pnpm install` + `pnpm --filter @automattic/jetpack-premium-analytics build` -to validate. +Then prepend this header, update pnpm-workspace.yaml, delete the old patch file, and run +`pnpm install` + `pnpm --filter @automattic/jetpack-premium-analytics build` to validate. +`@automattic/number-formatters` must keep `./package.json` in its `exports` for +convention discovery to pick it up. diff --git a/CHANGELOG.md b/CHANGELOG.md -index eabf9e8..81302b5 100644 +index eabf9e8b11..5cb96ddbdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md -@@ -4,6 +4,12 @@ - +@@ -4,6 +4,11 @@ + ## 0.13.0 (2026-04-29) - + +### Enhancements + +- Use each package's own `name` field as its script-module ID and externalize internal-package imports by exact name. Decouples script-module identity from `wpPlugin.packageNamespace`, so the npm name survives end-to-end (npm name === import specifier === script-module ID). No-op for Core; enables consumers whose owned npm scope differs from `packageNamespace` to keep a single identifier across npm, IDE, and the WordPress runtime. +- Discover script-module packages outside `./packages/` via convention. Any entry in the plugin's `dependencies` whose `package.json` declares `wpScriptModuleExports` is registered as a script module, bundled, and externalized under its own npm name. No new config; local packages still take precedence on name collision. -+- Scope the generated `wp_deregister_script_module()` calls to `@wordpress/*` IDs (PR #77465), so shared non-core packages registered by multiple wp-build plugins fall through to Core's idempotent first-wins semantics instead of last-plugin-wins collisions. + ### Bug Fixes - Update the optional `@wordpress/boot`, `@wordpress/route`, and `@wordpress/theme` peer dependency ranges to avoid blocking newer compatible package versions ([#77568](https://github.com/WordPress/gutenberg/pull/77568)). -@@ -20,12 +25,12 @@ - - ### Breaking Changes - --- `@wordpress/boot`, `@wordpress/route`, `@wordpress/theme`, and `@wordpress/private-apis` are no longer bundled. They are now expected to be provided by WordPress Core (7.0+) or the Gutenberg plugin. -+- `@wordpress/boot`, `@wordpress/route`, `@wordpress/theme`, and `@wordpress/private-apis` are no longer bundled. They are now expected to be provided by WordPress Core (7.0+) or the Gutenberg plugin. - - ### Enhancements - --- Avoid unexpected results when typecasting `IS_GUTENBERG_PLUGIN` and `IS_WORDPRESS_CORE` values to Booleans ([#75844](https://github.com/WordPress/gutenberg/pull/75844)). --- Skip PHP transforms during builds when building for WordPress Core ([#75844](https://github.com/WordPress/gutenberg/pull/75844)). -+- Avoid unexpected results when typecasting `IS_GUTENBERG_PLUGIN` and `IS_WORDPRESS_CORE` values to Booleans ([#75844](https://github.com/WordPress/gutenberg/pull/75844)). -+- Skip PHP transforms during builds when building for WordPress Core ([#75844](https://github.com/WordPress/gutenberg/pull/75844)). - - ## 0.9.0 (2026-03-04) - -@@ -33,25 +38,25 @@ - - ## 0.7.0 (2026-01-29) - --- Update documentation to describe `wpPlugin.name` --- Add `wpWorkers` field support for automatic worker bundling ([#74785](https://github.com/WordPress/gutenberg/pull/74785)). --- Add WASM inlining plugin for bundling WebAssembly modules. -+- Update documentation to describe `wpPlugin.name` -+- Add `wpWorkers` field support for automatic worker bundling ([#74785](https://github.com/WordPress/gutenberg/pull/74785)). -+- Add WASM inlining plugin for bundling WebAssembly modules. - - ## 0.6.0 (2026-01-16) - - ### Breaking Changes - --- Renamed generated PHP files to avoid `index.php` naming conflicts: -- - `build/index.php` → `build/build.php` -- - `build/modules/index.php` → `build/modules/registry.php` -- - `build/scripts/index.php` → `build/scripts/registry.php` -- - `build/styles/index.php` → `build/styles/registry.php` -- - `build/routes/index.php` → `build/routes/registry.php` --- All generated page functions now include the `{{PREFIX}}` (from `wpPlugin.name`) at the beginning: -- - `register_my_page_route()` → `my_plugin_register_my_page_route()` -- - `my_page_render_page()` → `my_plugin_my_page_render_page()` -- - And similarly for all other page functions --- Route registration now uses named functions instead of anonymous closures, allowing third-party developers to unhook them -+- Renamed generated PHP files to avoid `index.php` naming conflicts: -+ - `build/index.php` → `build/build.php` -+ - `build/modules/index.php` → `build/modules/registry.php` -+ - `build/scripts/index.php` → `build/scripts/registry.php` -+ - `build/styles/index.php` → `build/styles/registry.php` -+ - `build/routes/index.php` → `build/routes/registry.php` -+- All generated page functions now include the `{{PREFIX}}` (from `wpPlugin.name`) at the beginning: -+ - `register_my_page_route()` → `my_plugin_register_my_page_route()` -+ - `my_page_render_page()` → `my_plugin_my_page_render_page()` -+ - And similarly for all other page functions -+- Route registration now uses named functions instead of anonymous closures, allowing third-party developers to unhook them - - ## 0.4.0 (2025-11-26) - -@@ -61,9 +66,9 @@ - - ### New Features - --- Initial release of `@wordpress/build` package --- Transpilation support for TypeScript/JSX to CommonJS and ESM formats --- SCSS and CSS modules compilation with LTR and RTL support --- WordPress script and module bundling --- Automatic PHP registration file generation --- Watch mode for development -+- Initial release of `@wordpress/build` package -+- Transpilation support for TypeScript/JSX to CommonJS and ESM formats -+- SCSS and CSS modules compilation with LTR and RTL support -+- WordPress script and module bundling -+- Automatic PHP registration file generation -+- Watch mode for development diff --git a/lib/build.mjs b/lib/build.mjs -index dbf3cac..d11f6db 100755 +index dbf3cac6c3..d11f6db61c 100755 --- a/lib/build.mjs +++ b/lib/build.mjs @@ -4,8 +4,10 @@ @@ -541,62 +473,8 @@ index dbf3cac..d11f6db 100755 const watcher = chokidar.watch( watchPaths, { ignored: [ -diff --git a/lib/package-utils.mjs b/lib/package-utils.mjs -index 15b7dd0..be4daa7 100644 ---- a/lib/package-utils.mjs -+++ b/lib/package-utils.mjs -@@ -87,7 +87,48 @@ export function getPackageInfo( fullPackageName, resolveDir = null ) { - // Resolve from the package root context to get correct versions - const contextPath = path.join( packageRoot, 'package.json' ); - const require = createRequire( contextPath ); -- const resolved = require.resolve( `${ fullPackageName }/package.json` ); -+ -+ let resolved; -+ try { -+ // Preferred path: the package exposes ./package.json via `exports`. -+ resolved = require.resolve( `${ fullPackageName }/package.json` ); -+ } catch ( error ) { -+ const code = /** @type {NodeJS.ErrnoException} */ ( error ).code; -+ if ( -+ code !== 'MODULE_NOT_FOUND' && -+ code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED' -+ ) { -+ throw error; -+ } -+ -+ // Fallback: walk up node_modules and look for the package directly. -+ // Covers two real cases: -+ // - Packages whose `exports` field does not list `./package.json`. -+ // - Imports resolved by other means (tsconfig paths, esbuild aliases) -+ // where the package is genuinely not in node_modules; the walk-up -+ // finds nothing and we return `null` so callers fall through. -+ let searchDir = packageRoot; -+ const fsRoot = path.parse( searchDir ).root; -+ while ( searchDir !== fsRoot ) { -+ const directPath = path.join( -+ searchDir, -+ 'node_modules', -+ fullPackageName, -+ 'package.json' -+ ); -+ if ( existsSync( directPath ) ) { -+ resolved = directPath; -+ break; -+ } -+ searchDir = path.dirname( searchDir ); -+ } -+ -+ if ( ! resolved ) { -+ packageJsonCache.set( cacheKey, null ); -+ return null; -+ } -+ } -+ - const result = getPackageInfoFromFile( resolved ); - packageJsonCache.set( cacheKey, result ); - diff --git a/lib/wordpress-externals-plugin.mjs b/lib/wordpress-externals-plugin.mjs -index 2b7e01a..a07a310 100644 +index 2b7e01a66f..a07a310067 100644 --- a/lib/wordpress-externals-plugin.mjs +++ b/lib/wordpress-externals-plugin.mjs @@ -46,17 +46,19 @@ async function generateContentHash( @@ -709,19 +587,18 @@ index 2b7e01a..a07a310 100644 for ( const externalConfig of packageExternals ) { build.onResolve( diff --git a/templates/module-registration.php.template b/templates/module-registration.php.template -index d2c8712273..e86be5f324 100644 +index d2c8712273..a419c24a24 100644 --- a/templates/module-registration.php.template +++ b/templates/module-registration.php.template -@@ -42,8 +42,13 @@ function {{PREFIX}}_register_script_modules() { +@@ -42,8 +42,12 @@ function {{PREFIX}}_register_script_modules() { $asset = file_exists( $asset_path ) ? require $asset_path : array(); // Deregister first to override any previously registered version - // (e.g., Core's default modules when running as a plugin). - wp_deregister_script_module( $module['id'] ); -+ // (e.g., Core's default modules when running as a plugin). Scoped to -+ // `@wordpress/*`, the only namespace Core registers by default, so shared -+ // non-core modules fall through to Core's idempotent first-wins semantics -+ // instead of silent last-plugin-wins collisions across wp-build plugins. ++ // of Core's default script modules. Scoped to `@wordpress/*` — the ++ // only namespace Core registers by default — so plugin-local and ++ // shared packages preserve Core's idempotent "first-wins" semantics. + if ( str_starts_with( $module['id'], '@wordpress/' ) ) { + wp_deregister_script_module( $module['id'] ); + } @@ -729,33 +606,31 @@ index d2c8712273..e86be5f324 100644 wp_register_script_module( $module['id'], diff --git a/templates/routes-registration.php.template b/templates/routes-registration.php.template -index 7ab43f1eb1..8aeaf29d0f 100644 +index 7ab43f1eb1..3aa6918c11 100644 --- a/templates/routes-registration.php.template +++ b/templates/routes-registration.php.template -@@ -54,8 +54,12 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name +@@ -54,8 +54,11 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name $content_handle = '{{HANDLE_PREFIX}}/routes/' . $route['name'] . '/content'; $extension = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.js' : '.min.js'; // Deregister first to override any previously registered version - // (e.g., Core's default modules when running as a plugin). - wp_deregister_script_module( $content_handle ); -+ // (e.g., Core's default modules). Scoped to `@wordpress/*` so -+ // plugin-local route modules fall through to Core's idempotent -+ // first-wins semantics instead of last-plugin-wins collisions. ++ // of Core's default script modules. Scoped to `@wordpress/*` — the ++ // only namespace Core registers by default. + if ( str_starts_with( $content_handle, '@wordpress/' ) ) { + wp_deregister_script_module( $content_handle ); + } wp_register_script_module( $content_handle, $build_constants['build_url'] . 'routes/' . $route['name'] . '/content' . $extension, -@@ -73,8 +77,12 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name +@@ -73,8 +76,11 @@ function {{PREFIX}}_register_page_routes( $page_routes, $register_function_name $route_handle = '{{HANDLE_PREFIX}}/routes/' . $route['name'] . '/route'; $extension = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.js' : '.min.js'; // Deregister first to override any previously registered version - // (e.g., Core's default modules when running as a plugin). - wp_deregister_script_module( $route_handle ); -+ // (e.g., Core's default modules). Scoped to `@wordpress/*` so -+ // plugin-local route modules fall through to Core's idempotent -+ // first-wins semantics instead of last-plugin-wins collisions. ++ // of Core's default script modules. Scoped to `@wordpress/*` — the ++ // only namespace Core registers by default. + if ( str_starts_with( $route_handle, '@wordpress/' ) ) { + wp_deregister_script_module( $route_handle ); + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e88b0bcde44..2c2e7ade2e61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,7 @@ pnpmfileChecksum: sha256-KrBHPLv8FlPoa5OUGbvotku6TLmuhZiq22jdIe7yBHc= patchedDependencies: '@wordpress/build@0.13.0': - hash: fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f + hash: 94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722 path: .pnpm-patches/@wordpress__build@0.13.0.patch react-autosize-textarea: hash: 5c09e1dee59caaaba3871f9d722f93e56b41169db486b059597e8f8c788aa464 @@ -2126,7 +2126,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: ^4.24.0 version: 4.28.2 @@ -2741,7 +2741,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) '@wordpress/date': specifier: 5.46.0 version: 5.46.0 @@ -3449,7 +3449,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: ^4.24.0 version: 4.28.2 @@ -3790,7 +3790,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) '@wordpress/theme': specifier: 0.13.0 version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3863,7 +3863,7 @@ importers: version: 18.3.28 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: 4.28.2 version: 4.28.2 @@ -4069,7 +4069,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) autoprefixer: specifier: 10.4.20 version: 10.4.20(postcss@8.5.14) @@ -4296,7 +4296,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: ^4.24.0 version: 4.28.2 @@ -4719,7 +4719,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) autoprefixer: specifier: 10.4.20 version: 10.4.20(postcss@8.5.14) @@ -4792,7 +4792,7 @@ importers: version: 6.46.0 '@wordpress/build': specifier: 0.13.0 - version: 0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) '@wordpress/icons': specifier: ^13.0.0 version: 13.1.0(react@18.3.1) @@ -24115,7 +24115,7 @@ snapshots: '@wordpress/browserslist-config@6.46.0': {} - '@wordpress/build@0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.5.0(postcss@8.5.14) @@ -24141,7 +24141,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.5.0(postcss@8.5.14) @@ -24166,7 +24166,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.5.0(postcss@8.5.14) @@ -24193,7 +24193,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.13.0(patch_hash=fb5644e84cd1db78ba75dd57ec688e7dab2453b1e9e5a771c5e53753f59f253f)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.13.0(patch_hash=94143566ec91c8e57107f8cb272ffbbd521cf5fcd6d111ab953b3eb8b6635722)(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 autoprefixer: 10.5.0(postcss@8.5.14) From fb13c640c7c974ed5c019e109782a1627f385e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 1 Jun 2026 21:19:13 +0100 Subject: [PATCH 8/8] expose package.json in number-formatters exports lets wp-build convention discovery read the manifest without a resolver workaround --- .../changelog/update-wp-build-package-sources | 3 +-- projects/js-packages/number-formatters/package.json | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/js-packages/number-formatters/changelog/update-wp-build-package-sources b/projects/js-packages/number-formatters/changelog/update-wp-build-package-sources index e402479c01c1..547c72ce6cef 100644 --- a/projects/js-packages/number-formatters/changelog/update-wp-build-package-sources +++ b/projects/js-packages/number-formatters/changelog/update-wp-build-package-sources @@ -1,5 +1,4 @@ Significance: patch Type: changed -Comment: Patch significance. -Add wpScriptModuleExports field for wp-build script module support +Add wpScriptModuleExports and expose ./package.json in exports so wp-build convention discovery can register this as a shared script module. diff --git a/projects/js-packages/number-formatters/package.json b/projects/js-packages/number-formatters/package.json index 64cb7d79d0c8..6cb3ad32313d 100644 --- a/projects/js-packages/number-formatters/package.json +++ b/projects/js-packages/number-formatters/package.json @@ -19,7 +19,8 @@ "types": "./dist/types/index.d.ts", "import": "./dist/esm/index.js", "require": "./dist/cjs/index.cjs" - } + }, + "./package.json": "./package.json" }, "main": "./dist/cjs/index.cjs", "module": "./dist/esm/index.js",