Skip to content

[wrong fork] Use package.json name as the script-module ID source of truth#78810

Closed
retrofox wants to merge 2 commits into
WordPress:trunkfrom
retrofox:refactor/wp-build-name-as-module-id
Closed

[wrong fork] Use package.json name as the script-module ID source of truth#78810
retrofox wants to merge 2 commits into
WordPress:trunkfrom
retrofox:refactor/wp-build-name-as-module-id

Conversation

@retrofox
Copy link
Copy Markdown
Contributor

@retrofox retrofox commented May 29, 2026

Problem

wpPlugin.packageNamespace plays three roles in wp-build today:

  • handle/global identity
  • externalization regex
  • script-module ID shape.

In Core, the three roles collapse into a single identifier through alignment, not by design. Gutenberg owns the @wordpress scope; every package's directory name matches its npm name:

gutenberg/
├── package.json          # wpPlugin.packageNamespace: "wordpress"
└── packages/
    ├── blocks/           # name: "@wordpress/blocks"
    └── data/             # name: "@wordpress/data"

Specifier === name. wp-build, npm, the IDE, and wp_register_script_module all see @wordpress/blocks.

In an organization-owned monorepo with multiple plugins, that alignment breaks. Each plugin needs its own packageNamespace, but every package's npm name has to live under the org-owned scope (npm ownership, host-repo lint):

monorepo/
└── plugins/
    ├── plugin-a/
    │   ├── package.json  # wpPlugin.packageNamespace: "plugin-a"
    │   └── packages/
    │       └── feature/  # name: "@org/plugin-a-feature"
    └── plugin-b/
        ├── package.json  # wpPlugin.packageNamespace: "plugin-b"
        └── packages/
            └── feature/  # name: "@org/plugin-b-feature"

wp-build emits the specifier @plugin-a/feature; the npm name is @org/plugin-a-feature. Same package, two strings. Two costs follow: the IDE needs a tsconfig paths alias to bridge them, and the unregistered specifier triggers dependency-confusion scanners.

A second symptom of the same coupling: wp-build only discovers packages under ./packages/. Shared packages that live outside that directory (workspace siblings, npm-installed siblings) cannot be compiled as script modules, so they get inlined into every consumer's bundle.

wp_register_script_module( $id, ... ) already accepts arbitrary IDs and the PHP registry treats them as opaque strings. The runtime indirection layer exists; wp-build is the only piece tying identity back to a config-derived shortcut.

Proposal

Two convention-driven changes that close the identity-discovery loop together:

1. Identity from package.json#name. Read each script module's ID from the package itself. Every other modern bundler treats name as the source of truth.

-const scriptModuleId =
-    exportName === '.'
-        ? `@${ packageNamespace }/${ packageName }`
-        : `@${ packageNamespace }/${ packageName }/${ fileName }`;
+const packageId = packageJson.name || `@${ packageNamespace }/${ packageName }`;
+const scriptModuleId =
+    exportName === '.' ? packageId : `${ packageId }/${ fileName }`;

(build.mjs:727-730) Plus an exact-name onResolve handler in wordpress-externals-plugin.mjs that externalizes discovered packages by their actual name. The legacy regex pattern stays as a fallback for Core's prerequisites pipeline.

2. Discovery via wpScriptModuleExports. Beyond ./packages/<dir>/, 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 take first-match precedence on name collision.

The two changes are interlocked: identity decoupling makes discovery extension safe (a convention-discovered package keeps its npm identity, never gets renamed to @<packageNamespace>/<dir>); discovery extension is what makes identity decoupling materially useful (any plugin can now consume a shared script-module package living anywhere the package manager resolves it).

Compatibility

No-op for Core. Every Gutenberg package's name already matches the legacy derivation, and the root dependencies declares zero entries with wpScriptModuleExports, so both paths are no-ops. A full Gutenberg build on this branch produces a byte-identical build/modules/ tree compared to trunk (verified locally).

For consumers with the dual-naming pattern, migration is renaming the package's name field. The directory does not need to move. For consumers consuming shared script modules via inlined bundles today, the migration is adding wpScriptModuleExports to the shared package's package.json and letting the convention path pick it up.

Context

Replaces #77226 (which tried to extend discovery via a packageSources config). No new config here; one less role for packageNamespace, and #77225 closes via convention rather than configuration.

Relates to #78714 / #78715.

Decouples script-module identity from wpPlugin.packageNamespace by
reading each package's own name field as the script-module ID and
externalizing internal-package imports against an exact-name registry.

The PHP registry, the asset manifest, and wp_register_script_module
already treat IDs as opaque strings, so the npm name now survives
end-to-end (npm name === import specifier === script-module ID).

No-op for Core (every package's name already matches the legacy
derivation). Unblocks consumers whose owned npm scope differs from
packageNamespace from keeping a single identifier across pnpm, IDE,
and the WordPress runtime.

Replaces WordPress#77226 with a removal-of-coupling framing.
@retrofox retrofox self-assigned this May 29, 2026
…convention

Extends discovery so a plugin can pull in shared script-module packages
from outside ./packages/ without any wp-build config. Any entry in
package.json#dependencies whose own package.json declares
wpScriptModuleExports is registered as a script module, bundled, and
externalized under its actual npm name.

Refactors the PACKAGES registry from string[] to Map<key, PackageEntry>
so a single bundler pass handles local-source packages (transpiled +
bundled) and convention-discovered packages (bundled + externalized,
no transpilation). The packageJson name field already drives the
script-module ID after the previous commit, so convention-discovered
packages keep their npm identity end-to-end.

Together with the identity refactor, closes the path from "shared
package living anywhere the package manager can resolve it" to
"separately-registered script module with module_dependencies tracked
in .asset.php", with no new config surface.

Local packages still take first-match precedence on name collision.
Verified: Gutenberg's build is byte-identical (no dependencies entries
declare wpScriptModuleExports, so the convention path is a no-op for
Core).
@retrofox
Copy link
Copy Markdown
Contributor Author

Moved to #78822 so the branch lives on origin rather than a fork.

@retrofox retrofox closed this May 29, 2026
@retrofox retrofox changed the title Use package.json name as the script-module ID source of truth [wrong fork] Use package.json name as the script-module ID source of truth Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant