Skip to content

🐛 withLocalizeScript / withInlineScript routing is order-sensitive on the View fluent API #81

Description

@gfazioli

Summary

In v2, View::withLocalizeScript($handle, ...) and View::withInlineScript($handle, ...) decide their destination asset bucket (adminAssets vs adminAppsAssets) by checking adminAppsAssetsHasHandle($handle) at call time. That makes the fluent API order-sensitive: if the developer chains withLocalizeScript before withAdminAppsScript, the lookup fails and the localize/inline ends up in the wrong bucket. At render time, wp_localize_script() runs before wp_enqueue_script(), WordPress doesn't know about the handle yet, and silently drops the payload.

This is the same class of bug that #73 / v2.0.1 set out to fix for inline scripts, but that fix only works when the admin apps script is registered first.

Repro

A straightforward fluent chain many developers would write naturally:

return $plugin
  ->view('dashboard.index')
  ->withLocalizeScript('app', 'MyPluginData', [
    'nonce'   => wp_create_nonce('my-plugin'),
    'version' => $plugin->Version,
  ])
  ->withAdminAppsScript('app', true);

Observed: window.MyPluginData is undefined in the rendered page. React/JS code that reads window.MyPluginData.version crashes with Cannot read properties of undefined (reading 'version'). Same for withInlineScript.

Expected: the handle name is the same in both calls ('app'), so the framework should route the localize to adminAppsAssets regardless of chain order.

Workaround: swap the order, call withAdminAppsScript first. I just shipped this workaround across Scotty's v2 alignment (two call sites).

Root cause

src/View/View.php lines around 301 and 343:

public function withLocalizeScript($handle, $name, $l10n): View
{
  if (is_admin()) {
    if ($this->adminAppsAssetsHasHandle($handle)) {
      $this->adminAppsAssets->addLocalizeScript($handle, $name, $l10n);
    } else {
      $this->adminAssets->addLocalizeScript($handle, $name, $l10n);
    }
  }
  ...
}

The check is performed immediately against the current state of adminAppsAssets, which may be empty if the chain hasn't reached withAdminAppsScript yet.

Suggested fix

Make the routing lazy: instead of deciding the bucket at call time, record pending localize/inline intents and resolve them right before enqueue runs. Rough sketch:

  1. In withLocalizeScript, always stage the intent: $this->pendingLocalize[] = compact('handle', 'name', 'l10n'). Same for withInlineScript / withInlineStyle.
  2. Add a single resolveLocalizeRouting() called at the top of render (or from the same hook that currently flushes assets). For each pending intent, look up adminAppsAssetsHasHandle($handle) now and dispatch to the right bucket.
  3. Delete the early if (...hasHandle...) branch from each chained method.

Backwards-compatible, no API change, no new method on the public surface.

Alternative (smaller): document that withAdminAppsScript must be called before withLocalizeScript / withInlineScript and hard-error with an actionable message if the handle hasn't been registered yet. Less ergonomic but catches the silent-drop failure loudly.

Impact

Medium. Hits any plugin that writes the chained version in the "read left-to-right" order (describe the payload, then say "and yes, this is an admin app"). Symptom is a silent undefined global and client-side React crash — hard to trace back to the call order without diving into the framework source.

Filed while migrating Scotty to v2.0.3, where both the main admin page and the dashboard widget were hit by the same issue. Fixed on the Scotty side by swapping the call order (commit pending), but the framework should not require developers to remember this subtlety.

Target

v2.0.4 alongside #80 (webpack stub .d.ts glob) and any other v2 follow-ups that surface.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions