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:
- In
withLocalizeScript, always stage the intent: $this->pendingLocalize[] = compact('handle', 'name', 'l10n'). Same for withInlineScript / withInlineStyle.
- 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.
- 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.
Summary
In v2,
View::withLocalizeScript($handle, ...)andView::withInlineScript($handle, ...)decide their destination asset bucket (adminAssetsvsadminAppsAssets) by checkingadminAppsAssetsHasHandle($handle)at call time. That makes the fluent API order-sensitive: if the developer chainswithLocalizeScriptbeforewithAdminAppsScript, the lookup fails and the localize/inline ends up in the wrong bucket. At render time,wp_localize_script()runs beforewp_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:
Observed:
window.MyPluginDataisundefinedin the rendered page. React/JS code that readswindow.MyPluginData.versioncrashes withCannot read properties of undefined (reading 'version'). Same forwithInlineScript.Expected: the handle name is the same in both calls (
'app'), so the framework should route the localize toadminAppsAssetsregardless of chain order.Workaround: swap the order, call
withAdminAppsScriptfirst. I just shipped this workaround across Scotty's v2 alignment (two call sites).Root cause
src/View/View.phplines around 301 and 343:The check is performed immediately against the current state of
adminAppsAssets, which may be empty if the chain hasn't reachedwithAdminAppsScriptyet.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:
withLocalizeScript, always stage the intent:$this->pendingLocalize[] = compact('handle', 'name', 'l10n'). Same forwithInlineScript/withInlineStyle.resolveLocalizeRouting()called at the top of render (or from the same hook that currently flushes assets). For each pending intent, look upadminAppsAssetsHasHandle($handle)now and dispatch to the right bucket.if (...hasHandle...)branch from each chained method.Backwards-compatible, no API change, no new method on the public surface.
Alternative (smaller): document that
withAdminAppsScriptmust be called beforewithLocalizeScript/withInlineScriptand 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.tsglob) and any other v2 follow-ups that surface.