From 422e4fef07e5e1985709d25da80df548dd708a06 Mon Sep 17 00:00:00 2001 From: Khokan Sardar Date: Sun, 7 Jun 2026 11:05:27 +0530 Subject: [PATCH 1/3] Add block-theme template edit URL resolution to lib/rest.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Template-backed views (blog index, archives) have no post.php/term.php destination — on block themes they edit a site-editor template instead. Adds pure, testable helpers (templateCandidates, pickTemplate, buildSiteEditorUrl) and the REST orchestrator resolveTemplateEditUrlAsync, which gates on the active theme's is_block_theme flag and matches the current view against /wp/v2/templates following the template hierarchy. Returns { url, isBlockTheme } so the popup can label the disabled state honestly (classic theme vs. block theme with no matching template vs. undeterminable). Smoke tests cover candidate ordering, template matching with fallback, URL construction, and the full block/classic/unauth paths. --- lib/rest.js | 122 ++++++++++++++++++++++++++++++++++++++++ test/smoke.js | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) diff --git a/lib/rest.js b/lib/rest.js index 24cf393..37ff41f 100644 --- a/lib/rest.js +++ b/lib/rest.js @@ -157,6 +157,122 @@ return null; } + // --- Template-backed views (block themes) ------------------------------- + + /** + * Page types that are rendered from a block-theme template/template-part + * rather than a single editable post: the blog index and archives. These + * have no post.php / term.php destination — they resolve to a site-editor + * deep link instead. Category/tag (`pageType === 'term'`) and author + * archives are intentionally excluded: they have their own editable + * record (the term / user) and resolve via the sync/REST paths above. + */ + function isTemplateBackedPage(ctx) { + return ctx.pageType === 'home' || ctx.pageType === 'archive'; + } + + /** + * Ordered template-slug candidates for a template-backed view, following + * WordPress's template hierarchy from most to least specific. The caller + * picks the first candidate that the active theme actually registers. + * + * home → home, index (blog posts index) + * archive → archive-{postType}?, archive, index + * + * A static front page or posts page is a real Page (pageType 'single') + * and never reaches here — it resolves to post.php upstream. + */ + function templateCandidates(ctx) { + if (ctx.pageType === 'home') { + return ['home', 'index']; + } + if (ctx.pageType === 'archive') { + const candidates = []; + if (ctx.postType) candidates.push(`archive-${ctx.postType}`); + candidates.push('archive', 'index'); + return candidates; + } + return []; + } + + /** + * Given the registered-template list from /wp/v2/templates, returns the + * most specific template matching the current view, or null. Each template + * object carries an `id` of the form `{stylesheet}//{slug}`, which is + * exactly what the site editor's `postId` expects. + */ + function pickTemplate(ctx, templates) { + if (!Array.isArray(templates)) return null; + const bySlug = new Map(); + for (const t of templates) { + if (t && typeof t.slug === 'string' && t.id) bySlug.set(t.slug, t); + } + for (const slug of templateCandidates(ctx)) { + if (bySlug.has(slug)) return bySlug.get(slug); + } + return null; + } + + /** + * Builds the site-editor deep link for a resolved template. `canvas=edit` + * opens straight into edit mode rather than the template's preview screen. + * The template `id` is already `{stylesheet}//{slug}`; encode it so the + * `//` survives as the postId value. + */ + function buildSiteEditorUrl(origin, template) { + if (!template || !template.id) return null; + const postId = encodeURIComponent(template.id); + return `${origin}/wp-admin/site-editor.php?postType=wp_template&postId=${postId}&canvas=edit`; + } + + /** + * Lists the active theme's registered templates. Private endpoint — + * requires edit_theme_options (admins) and a valid X-WP-Nonce. Returns + * an array (possibly empty) or null on failure / insufficient caps. + */ + async function fetchTemplates({ restApiRoot, origin, nonce, fetchImpl = fetch }) { + const root = normalizeRoot(restApiRoot, origin); + try { + const res = await fetchImpl(`${root}wp/v2/templates`, { + credentials: 'include', + headers: nonce ? { 'X-WP-Nonce': nonce } : undefined, + }); + if (!res.ok) return null; + const data = await res.json(); + return Array.isArray(data) ? data : null; + } catch (_) { + return null; + } + } + + /** + * Resolves a template-backed view to a site-editor edit URL. Returns + * `{ url, isBlockTheme }` so the popup can label the disabled state + * honestly: + * + * - isBlockTheme false → classic theme; templates are PHP files, no URL. + * - isBlockTheme true, url null → block theme but no matching template. + * - isBlockTheme null → couldn't determine (not an admin, REST off). + * + * Reads the active theme's `is_block_theme` flag first (cheap gate) and + * only lists templates when it's a block theme. + */ + async function resolveTemplateEditUrlAsync({ ctx, origin, nonce, fetchImpl = fetch }) { + if (!isTemplateBackedPage(ctx)) return { url: null, isBlockTheme: null }; + + const theme = await fetchActiveTheme({ + restApiRoot: ctx.restApiRoot, origin, nonce, fetchImpl, + }); + const isBlockTheme = theme ? !!theme.is_block_theme : null; + if (isBlockTheme !== true) return { url: null, isBlockTheme }; + + const templates = await fetchTemplates({ + restApiRoot: ctx.restApiRoot, origin, nonce, fetchImpl, + }); + const url = buildSiteEditorUrl(origin, pickTemplate(ctx, templates)); + return { url: url || null, isBlockTheme: true }; + } + /** * Sync-only resolution — no network. Returns the best admin URL given * whatever IDs we already have in context, or null. @@ -341,6 +457,12 @@ resolveEditUrlSync, resolveEditUrlAsync, canResolveViaRest, + isTemplateBackedPage, + templateCandidates, + pickTemplate, + buildSiteEditorUrl, + fetchTemplates, + resolveTemplateEditUrlAsync, fetchSiteInfo, fetchActiveTheme, fetchPluginsDetail, diff --git a/test/smoke.js b/test/smoke.js index 198ea23..36a9a2a 100644 --- a/test/smoke.js +++ b/test/smoke.js @@ -713,6 +713,158 @@ async function main() { 'plain logged-in user (no network admin menu) is not flagged as super admin'); } + // --- 22. Template-backed views — candidate slugs ---------------------- + { + console.log('\n[22] templateCandidates — hierarchy per page type'); + const dom = new JSDOM(``); + const ctx = loadModules(dom); + const cand = ctx.WPRest.templateCandidates; + + assert(JSON.stringify(cand({ pageType: 'home' })) === JSON.stringify(['home', 'index']), + 'home → [home, index]'); + assert(JSON.stringify(cand({ pageType: 'archive' })) === JSON.stringify(['archive', 'index']), + 'bare archive → [archive, index]'); + assert(JSON.stringify(cand({ pageType: 'archive', postType: 'book' })) + === JSON.stringify(['archive-book', 'archive', 'index']), + 'post-type archive → [archive-book, archive, index]'); + assert(cand({ pageType: 'term' }).length === 0, + 'term page type yields no template candidates (handled by term.php)'); + assert(cand({ pageType: 'single' }).length === 0, 'single yields none'); + + assert(ctx.WPRest.isTemplateBackedPage({ pageType: 'home' }) === true, 'home is template-backed'); + assert(ctx.WPRest.isTemplateBackedPage({ pageType: 'archive' }) === true, 'archive is template-backed'); + assert(ctx.WPRest.isTemplateBackedPage({ pageType: 'term' }) === false, 'term is NOT template-backed'); + } + + // --- 23. pickTemplate matches the most specific registered slug -------- + { + console.log('\n[23] pickTemplate — most specific registered template wins'); + const dom = new JSDOM(``); + const ctx = loadModules(dom); + const pick = ctx.WPRest.pickTemplate; + + const templates = [ + { id: 'twentytwentyfour//index', slug: 'index' }, + { id: 'twentytwentyfour//archive', slug: 'archive' }, + { id: 'twentytwentyfour//home', slug: 'home' }, + ]; + + assert(pick({ pageType: 'home' }, templates).slug === 'home', + 'home view picks the home template over index'); + assert(pick({ pageType: 'archive' }, templates).slug === 'archive', + 'archive view picks the archive template'); + // No archive-book registered → falls back to archive. + assert(pick({ pageType: 'archive', postType: 'book' }, templates).slug === 'archive', + 'post-type archive falls back to archive when archive-book absent'); + // Only index registered → home falls all the way back to index. + assert(pick({ pageType: 'home' }, [{ id: 'x//index', slug: 'index' }]).slug === 'index', + 'home falls back to index when home template absent'); + assert(pick({ pageType: 'home' }, []) === null, 'no templates → null'); + assert(pick({ pageType: 'home' }, null) === null, 'null templates → null'); + // Templates missing an id are ignored (can't build a postId from them). + assert(pick({ pageType: 'home' }, [{ slug: 'home' }]) === null, + 'template without id is skipped'); + } + + // --- 24. buildSiteEditorUrl encodes the template id ------------------- + { + console.log('\n[24] buildSiteEditorUrl — site editor deep link'); + const dom = new JSDOM(``); + const ctx = loadModules(dom); + const build = ctx.WPRest.buildSiteEditorUrl; + + const url = build('https://example.com', { id: 'twentytwentyfour//home', slug: 'home' }); + assert(url === 'https://example.com/wp-admin/site-editor.php?postType=wp_template&postId=twentytwentyfour%2F%2Fhome&canvas=edit', + `deep link built + id encoded: ${url}`); + assert(build('https://example.com', null) === null, 'null template → null URL'); + assert(build('https://example.com', { slug: 'home' }) === null, 'template without id → null URL'); + } + + // --- 25. resolveTemplateEditUrlAsync — block vs classic theme --------- + { + console.log('\n[25] resolveTemplateEditUrlAsync — full block-theme resolution'); + const dom = new JSDOM(``); + const ctx = loadModules(dom); + + // Block theme: /themes?status=active reports is_block_theme, then + // /templates lists the registered templates. + const blockFetch = async (url, options) => { + if (url.includes('/wp/v2/themes')) { + return { ok: true, async json() { return [{ stylesheet: 'twentytwentyfour', is_block_theme: true }]; } }; + } + if (url.includes('/wp/v2/templates')) { + return { + ok: true, + async json() { + return [ + { id: 'twentytwentyfour//index', slug: 'index' }, + { id: 'twentytwentyfour//home', slug: 'home' }, + ]; + }, + }; + } + return { ok: false }; + }; + + const blogHome = await ctx.WPRest.resolveTemplateEditUrlAsync({ + ctx: { pageType: 'home', restApiRoot: 'https://example.com/wp-json/' }, + origin: 'https://example.com', + nonce: 'deadbeef', + fetchImpl: blockFetch, + }); + assert(blogHome.isBlockTheme === true, 'block theme detected via is_block_theme'); + assert(blogHome.url === 'https://example.com/wp-admin/site-editor.php?postType=wp_template&postId=twentytwentyfour%2F%2Fhome&canvas=edit', + `blog index resolves to the home template: ${blogHome.url}`); + + // Archive on the same block theme → falls back to the index template + // (no archive template registered above). + const archive = await ctx.WPRest.resolveTemplateEditUrlAsync({ + ctx: { pageType: 'archive', restApiRoot: 'https://example.com/wp-json/' }, + origin: 'https://example.com', + nonce: 'deadbeef', + fetchImpl: blockFetch, + }); + assert(archive.url && archive.url.includes('postId=twentytwentyfour%2F%2Findex'), + `archive falls back to index template: ${archive.url}`); + + // Classic theme: is_block_theme false → no URL, honest flag. + const classicFetch = async (url) => { + if (url.includes('/wp/v2/themes')) { + return { ok: true, async json() { return [{ stylesheet: 'twentytwentyone', is_block_theme: false }]; } }; + } + return { ok: false }; + }; + const classic = await ctx.WPRest.resolveTemplateEditUrlAsync({ + ctx: { pageType: 'home', restApiRoot: 'https://example.com/wp-json/' }, + origin: 'https://example.com', + nonce: 'deadbeef', + fetchImpl: classicFetch, + }); + assert(classic.isBlockTheme === false && classic.url === null, + 'classic theme → no URL, isBlockTheme=false'); + + // Theme lookup fails (non-admin / REST off) → isBlockTheme null. + const unauthFetch = async () => ({ ok: false }); + const unknown = await ctx.WPRest.resolveTemplateEditUrlAsync({ + ctx: { pageType: 'home', restApiRoot: 'https://example.com/wp-json/' }, + origin: 'https://example.com', + fetchImpl: unauthFetch, + }); + assert(unknown.isBlockTheme === null && unknown.url === null, + 'undeterminable theme → isBlockTheme=null, url=null'); + + // Non-template-backed page short-circuits without any fetch. + let touched = false; + const guardFetch = async () => { touched = true; return { ok: false }; }; + const term = await ctx.WPRest.resolveTemplateEditUrlAsync({ + ctx: { pageType: 'term', restApiRoot: 'https://example.com/wp-json/' }, + origin: 'https://example.com', + fetchImpl: guardFetch, + }); + assert(term.url === null && touched === false, + 'term page short-circuits — no REST calls made'); + } + // --- 12. Not a WordPress site ----------------------------------------- { console.log('\n[12] Non-WordPress page'); From 041a31669c311fe49329326a7d09bfaffc8cd18b Mon Sep 17 00:00:00 2001 From: Khokan Sardar Date: Sun, 7 Jun 2026 11:12:16 +0530 Subject: [PATCH 2/3] Wire popup Edit button to block-theme site editor for templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Template-backed views (blog index, archives) were disabled with "(Coming Soon)" placeholders. The popup now resolves them to a site-editor deep link on block themes: - content.js handles RESOLVE_TEMPLATE_EDIT_URL, calling the new resolveTemplateEditUrlAsync with the popup-supplied REST nonce (the /themes and /templates endpoints are private). - actions.js adds requestTemplateEditUrl, reusing the existing nonce resolution path (same as site-info / current-user). - useEditUrlResolution gains a template branch and surfaces isBlockTheme. - Labels are now honest about the disabled state: classic theme, block theme with no matching template, or undeterminable — never a "Coming Soon" false promise. Enabled rows read "Edit Blog Template" / "Edit Archive Template". Category/tag and author archives are unchanged — they keep editing their own term/user record via term.php / user-edit.php. --- content.js | 13 ++++++++++ dist/popup.js | 2 +- src/popup/components/DetectedView.js | 38 ++++++++++++++++++++++------ src/popup/lib/actions.js | 16 ++++++++++++ src/popup/lib/labels.js | 22 +++++++++++++--- 5 files changed, 79 insertions(+), 12 deletions(-) diff --git a/content.js b/content.js index bc82059..22a1ab7 100644 --- a/content.js +++ b/content.js @@ -242,6 +242,19 @@ return true; } + if (msg.type === 'RESOLVE_TEMPLATE_EDIT_URL') { + // Template-backed views (blog index, archives) on block themes resolve + // to a site-editor deep link. Needs an authenticated REST round-trip + // (/themes + /templates are private), so the popup passes the nonce it + // already resolves for the site-info/user endpoints. + const nonce = msg.nonce || null; + globalThis.WPRest + .resolveTemplateEditUrlAsync({ ctx: detection.context, origin: location.origin, nonce }) + .then((res) => sendResponse(res || { url: null, isBlockTheme: null })) + .catch(() => sendResponse({ url: null, isBlockTheme: null })); + return true; + } + if (msg.type === 'GET_SITE_INFO') { // Runs from the page context, so cookies flow automatically. The popup // pre-reads window.wpApiSettings.nonce via chrome.scripting (MAIN world, diff --git a/dist/popup.js b/dist/popup.js index 686abc0..ff7fb3a 100644 --- a/dist/popup.js +++ b/dist/popup.js @@ -1 +1 @@ -(()=>{var e,t,n={20:(e,t,n)=>{"use strict";var r=n(540),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,l=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,n){var r,a={},c=null,u=null;for(r in void 0!==n&&(c=""+n),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)i.call(t,r)&&!s.hasOwnProperty(r)&&(a[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===a[r]&&(a[r]=t[r]);return{$$typeof:o,type:e,key:c,ref:u,props:a,_owner:l.current}}t.Fragment=a,t.jsx=c,t.jsxs=c},162:(e,t,n)=>{"use strict";var r=n(540),o=n(888),a="function"==typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e==1/t)||e!=e&&t!=t},i=o.useSyncExternalStore,l=r.useRef,s=r.useEffect,c=r.useMemo,u=r.useDebugValue;t.useSyncExternalStoreWithSelector=function(e,t,n,r,o){var d=l(null);if(null===d.current){var f={hasValue:!1,value:null};d.current=f}else f=d.current;d=c(function(){function e(e){if(!s){if(s=!0,i=e,e=r(e),void 0!==o&&f.hasValue){var t=f.value;if(o(t,e))return l=t}return l=e}if(t=l,a(i,e))return t;var n=r(e);return void 0!==o&&o(t,n)?(i=e,t):(i=e,l=n)}var i,l,s=!1,c=void 0===n?null:n;return[function(){return e(t())},null===c?void 0:function(){return e(c())}]},[t,n,r,o]);var p=i(e,d[0],d[1]);return s(function(){f.hasValue=!0,f.value=p},[p]),u(p),p}},242:(e,t,n)=>{"use strict";e.exports=n(162)},287:(e,t)=>{"use strict";var n=Symbol.for("react.element"),r=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.provider"),s=Symbol.for("react.context"),c=Symbol.for("react.forward_ref"),u=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),f=Symbol.for("react.lazy"),p=Symbol.iterator,h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},m=Object.assign,g={};function b(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||h}function v(){}function y(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||h}b.prototype.isReactComponent={},b.prototype.setState=function(e,t){if("object"!=typeof e&&"function"!=typeof e&&null!=e)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")},b.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")},v.prototype=b.prototype;var w=y.prototype=new v;w.constructor=y,m(w,b.prototype),w.isPureReactComponent=!0;var x=Array.isArray,k=Object.prototype.hasOwnProperty,_={current:null},S={key:!0,ref:!0,__self:!0,__source:!0};function E(e,t,r){var o,a={},i=null,l=null;if(null!=t)for(o in void 0!==t.ref&&(l=t.ref),void 0!==t.key&&(i=""+t.key),t)k.call(t,o)&&!S.hasOwnProperty(o)&&(a[o]=t[o]);var s=arguments.length-2;if(1===s)a.children=r;else if(1{!function(){"use strict";const t=new Set(["wp/v2","wp/v2/fields","wp-site-health/v1","oembed/1.0","wp-block-editor/v1","akismet/v1"]);function n(e){if("string"!=typeof e||!e)return null;try{const t=new URL(e).protocol;if("http:"===t||"https:"===t)return e}catch(e){}return null}const r={mergePlugins:function(e,r,o){const a=new Map;for(const t of e)a.set(t,{slug:t,name:null,version:null,active:null,pluginUri:null});for(const e of o||[]){if(t.has(e))continue;const n=e.split("/")[0];n&&"wp"!==n&&(a.has(n)||a.set(n,{slug:n,name:null,version:null,active:null,pluginUri:null}))}for(const e of r||[]){const t=(e.plugin||"").split("/")[0];if(!t)continue;const r={slug:t,name:e.name||null,version:e.version||null,active:"active"===e.status,pluginUri:n(e.plugin_uri)};a.set(t,r)}return Array.from(a.values()).filter(e=>!1!==e.active).sort((e,t)=>e.slug.localeCompare(t.slug))},mergeTheme:function(e,t){return e||t?t?{slug:t.stylesheet||e,name:t.name?.rendered||t.name||e,version:t.version||null,author:t.author?.rendered||t.author||null}:{slug:e,name:e,version:null,author:null}:null},stripTags:function(e){return"string"!=typeof e?"":e.replace(/<[^>]*>/g,"").trim()}};globalThis.WPSiteInfo=r,e.exports&&(e.exports=r)}()},338:(e,t,n)=>{"use strict";var r=n(961);t.H=r.createRoot,r.hydrateRoot},463:(e,t)=>{"use strict";function n(e,t){var n=e.length;e.push(t);e:for(;0>>1,o=e[r];if(!(0>>1;ra(s,n))ca(u,s)?(e[r]=u,e[c]=n,r=c):(e[r]=s,e[l]=n,r=l);else{if(!(ca(u,n)))break e;e[r]=u,e[c]=n,r=c}}}return t}function a(e,t){var n=e.sortIndex-t.sortIndex;return 0!==n?n:e.id-t.id}if("object"==typeof performance&&"function"==typeof performance.now){var i=performance;t.unstable_now=function(){return i.now()}}else{var l=Date,s=l.now();t.unstable_now=function(){return l.now()-s}}var c=[],u=[],d=1,f=null,p=3,h=!1,m=!1,g=!1,b="function"==typeof setTimeout?setTimeout:null,v="function"==typeof clearTimeout?clearTimeout:null,y="undefined"!=typeof setImmediate?setImmediate:null;function w(e){for(var t=r(u);null!==t;){if(null===t.callback)o(u);else{if(!(t.startTime<=e))break;o(u),t.sortIndex=t.expirationTime,n(c,t)}t=r(u)}}function x(e){if(g=!1,w(e),!m)if(null!==r(c))m=!0,j(k);else{var t=r(u);null!==t&&z(x,t.startTime-e)}}function k(e,n){m=!1,g&&(g=!1,v(C),C=-1),h=!0;var a=p;try{for(w(n),f=r(c);null!==f&&(!(f.expirationTime>n)||e&&!T());){var i=f.callback;if("function"==typeof i){f.callback=null,p=f.priorityLevel;var l=i(f.expirationTime<=n);n=t.unstable_now(),"function"==typeof l?f.callback=l:f===r(c)&&o(c),w(n)}else o(c);f=r(c)}if(null!==f)var s=!0;else{var d=r(u);null!==d&&z(x,d.startTime-n),s=!1}return s}finally{f=null,p=a,h=!1}}"undefined"!=typeof navigator&&void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var _,S=!1,E=null,C=-1,R=5,M=-1;function T(){return!(t.unstable_now()-Me||125i?(e.sortIndex=a,n(u,e),null===r(c)&&e===r(u)&&(g?(v(C),C=-1):g=!0,z(x,a-i))):(e.sortIndex=l,n(c,e),m||h||(m=!0,j(k))),e},t.unstable_shouldYield=T,t.unstable_wrapCallback=function(e){var t=p;return function(){var n=p;p=t;try{return e.apply(this,arguments)}finally{p=n}}}},493:(e,t,n)=>{"use strict";var r=n(540),o="function"==typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e==1/t)||e!=e&&t!=t},a=r.useState,i=r.useEffect,l=r.useLayoutEffect,s=r.useDebugValue;function c(e){var t=e.getSnapshot;e=e.value;try{var n=t();return!o(e,n)}catch(e){return!0}}var u="undefined"==typeof window||void 0===window.document||void 0===window.document.createElement?function(e,t){return t()}:function(e,t){var n=t(),r=a({inst:{value:n,getSnapshot:t}}),o=r[0].inst,u=r[1];return l(function(){o.value=n,o.getSnapshot=t,c(o)&&u({inst:o})},[e,n,t]),i(function(){return c(o)&&u({inst:o}),e(function(){c(o)&&u({inst:o})})},[e]),s(n),n};t.useSyncExternalStore=void 0!==r.useSyncExternalStore?r.useSyncExternalStore:u},540:(e,t,n)=>{"use strict";e.exports=n(287)},551:(e,t,n)=>{"use strict";var r=n(540),o=n(982);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n