@symbo.ls/utils 3.14.774 → 3.14.776

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{isObject as a,isString as y}from"./types.js";const m="system",v="https://smbls-kv.nika-980.workers.dev",O="https://api.symbols.app";function f(n){const t=String(n||"").trim();if(!t)return{owner:m,key:"",full:""};let r=null,e=t;if(t.includes("/")){const o=t.indexOf("/");r=t.slice(0,o),e=t.slice(o+1)}e=e.replace(/\.symbo\.ls$/iu,""),r||(r=m);const s=e?`${r}/${e}`:"";return{owner:r,key:e,full:s}}const d=(n,t,r,e)=>{for(const s in t){const o=e?`${e}/${s}`:s;r&&r.has(o)||(s in n?a(n[s])&&a(t[s])&&d(n[s],t[s],r,o):n[s]=t[s])}},h=(n,t,r)=>{if(!a(n)||!t.size)return n;const e=`${r}/`;let s=!1;for(const i of t)if(i.startsWith(e)){s=!0;break}if(!s)return n;const o={};for(const i in n){if(!Object.prototype.hasOwnProperty.call(n,i))continue;const c=`${r}/${i}`;t.has(c)||(o[i]=h(n[i],t,c))}return o},j=new Set,F=new Set(["components","pages","functions","snippets","methods"]),b=(n,t)=>t===""&&F.has(n),w=["fontFamily","fontfamily","font_family","FONT_FAMILY"],R=n=>{const t=new Map;if(!a(n))return t;for(const r of w){const e=n[r];if(a(e))for(const s in e)!a(e[s])||!e[s].isDefault||(t.has(r)||t.set(r,new Set),t.get(r).add(s))}return t},U=(n,t)=>{for(const r of w){const e=n[r];if(!a(e))continue;const s=t.get(r);let o=null;for(const i in e){const c=e[i];if(!a(c)||!c.isDefault||s&&s.has(i))continue;const l={...c};delete l.isDefault,o||(o={...e}),o[i]=l}o&&(n[r]=o)}},E=n=>n.replace(/\.js$/iu,""),g=n=>{const t=new Set;if(!Array.isArray(n))return t;for(const r of n){if(!y(r))continue;const e=r.split("/").map(s=>E(s.trim())).filter(Boolean).join("/");e&&t.add(e)}return t},G=(n,t={})=>({library:n,ignoreList:Array.isArray(t.ignoreList)?t.ignoreList:[]}),k=n=>a(n)&&Array.isArray(n.ignoreList)&&("library"in n||"key"in n||"name"in n),B=n=>n.library!==void 0?n.library:n.key??n.name,P=(n,t,r)=>{const e=g(t.sharedLibIgnore);t.sharePages===!1&&e.add("pages");const s=e.size?new Set([...r,...e]):r;for(const o in t)if(Object.prototype.hasOwnProperty.call(t,o)&&!(o==="sharePages"||o==="sharedLibIgnore")&&o!=="app"&&!s.has(o))if(a(t[o])&&a(n[o]))if(o==="designSystem")d(n[o],t[o],s,o);else for(const i in t[o])s.has(`${o}/${i}`)||(i in n[o]?b(o,n[o][i])&&!b(o,t[o][i])&&(n[o][i]=t[o][i]):n[o][i]=t[o][i]);else o in n||(n[o]=h(t[o],s,o))},N=(n,t)=>{if(!t||!t.length)return;const r=R(n.designSystem);for(let e=0;e<t.length;e++){let s=t[e],o=j;k(s)&&(o=g(s.ignoreList),s=s.library),a(s)&&P(n,s,o)}r.size&&U(n.designSystem,r)};async function D(n,t={}){if((t.provider||"kv")==="api")return L(n,t);const e=await T(n,t);return e||L(n,t)}async function T(n,t={}){const r=t.kvBaseUrl||v,e=t.env||"production",s=`${r}/kv/${encodeURIComponent(n)}?env=${e}`;try{const o=await fetch(s,{method:"GET"});return o.ok&&(await o.json())?.value||null}catch{return null}}async function L(n,t={}){const r=t.apiBaseUrl||O,{key:e}=f(n);try{const s=`${r}/core/projects/libraries/available?search=${encodeURIComponent(e)}&limit=10`,o=await fetch(s,{method:"GET"});if(!o.ok)return null;const i=await o.json(),l=(i?.items||i?.data||(Array.isArray(i)?i:[])).find(I=>{const{key:_}=f(I?.key);return _.toLowerCase()===e.toLowerCase()});if(!l?.id&&!l?._id)return null;const A=l.id||l._id,S=`${r}/core/projects/${encodeURIComponent(A)}/data?branch=main`,u=await fetch(S,{method:"GET"});if(!u.ok)return null;const p=await u.json();return p?.data||p||null}catch{return null}}async function $(n,t={}){if(a(n))return n;if(y(n)){const{full:r}=f(n);if(!r)return null;try{const e=await D(r,t);return e||console.warn(`[smbls] Shared library "${r}" not found`),e}catch(e){return console.warn(`[smbls] Failed to fetch shared library "${r}":`,e.message),null}}return null}async function z(n,t={}){return!n||!n.length?[]:(await Promise.all(n.map(async e=>{if(k(e)){const s=await $(B(e),t);return s?{library:s,ignoreList:e.ignoreList}:null}return $(e,t)}))).filter(Boolean)}export{d as deepDefaults,D as fetchLibraryData,k as isWrappedLibrary,N as mergeSharedLibraries,g as normalizeIgnoreList,f as normalizeLibraryKey,z as resolveSharedLibraries,G as sharedLibrary};
1
+ import{isObject as l,isString as u}from"./types.js";import{isProtectedElementMethod as d}from"./keys.js";import{isLoudDevRuntime as O}from"./env.js";const w="system",B="https://smbls-kv.nika-980.workers.dev",E="https://api.symbols.app";function h(n){const t=String(n||"").trim();if(!t)return{owner:w,key:"",full:""};let o=null,e=t;if(t.includes("/")){const r=t.indexOf("/");o=t.slice(0,r),e=t.slice(r+1)}e=e.replace(/\.symbo\.ls$/iu,""),o||(o=w);const s=e?`${o}/${e}`:"";return{owner:o,key:e,full:s}}const b=(n,t,o,e)=>{for(const s in t){const r=e?`${e}/${s}`:s;o&&o.has(r)||(s in n?l(n[s])&&l(t[s])&&b(n[s],t[s],o,r):n[s]=t[s])}},g=(n,t,o)=>{if(!l(n)||!t.size)return n;const e=`${o}/`;let s=!1;for(const a of t)if(a.startsWith(e)){s=!0;break}if(!s)return n;const r={};for(const a in n){if(!Object.prototype.hasOwnProperty.call(n,a))continue;const i=`${o}/${a}`;t.has(i)||(r[a]=g(n[a],t,i))}return r},F=new Set,U=new Set(["components","pages","functions","snippets","methods"]),k=(n,t)=>t===""&&U.has(n),$=["fontFamily","fontfamily","font_family","FONT_FAMILY"],P=n=>{const t=new Map;if(!l(n))return t;for(const o of $){const e=n[o];if(l(e))for(const s in e)!l(e[s])||!e[s].isDefault||(t.has(o)||t.set(o,new Set),t.get(o).add(s))}return t},M=(n,t)=>{for(const o of $){const e=n[o];if(!l(e))continue;const s=t.get(o);let r=null;for(const a in e){const i=e[a];if(!l(i)||!i.isDefault||s&&s.has(a))continue;const c={...i};delete c.isDefault,r||(r={...e}),r[a]=c}r&&(n[o]=r)}},D=n=>n.replace(/\.js$/iu,""),L=n=>{const t=new Set;if(!Array.isArray(n))return t;for(const o of n){if(!u(o))continue;const e=o.split("/").map(s=>D(s.trim())).filter(Boolean).join("/");e&&t.add(e)}return t},x=(n,t={})=>({library:n,ignoreList:Array.isArray(t.ignoreList)?t.ignoreList:[]}),A=n=>l(n)&&Array.isArray(n.ignoreList)&&("library"in n||"key"in n||"name"in n),S=n=>n.library!==void 0?n.library:n.key??n.name,m=new Set,V=()=>{m.clear()},T=(n,t)=>{const o=t.filter(e=>!m.has(e));if(o.length){for(const e of o)m.add(e);O()&&console.warn(`[smbls] shared library "${n}" ships ${o.map(e=>`methods.${e}`).join(", ")} \u2014 named like a built-in element method. Refused: the built-in wins; a library cannot replace an element method on the consuming app. If the app wants this version, declare it in the app's OWN methods.`)}},C=(n,t,o)=>{let e=null;for(const s in n)if(d(s)&&(t.has(`methods/${s}`)||o.push(s),!e)){e={};for(const r in n)d(r)||(e[r]=n[r])}return e||n},N=(n,t,o,e="library")=>{const s=L(t.sharedLibIgnore);t.sharePages===!1&&s.add("pages");const r=s.size?new Set([...o,...s]):o,a=[];for(const i in t)if(Object.prototype.hasOwnProperty.call(t,i)&&!(i==="sharePages"||i==="sharedLibIgnore")&&i!=="app"&&!r.has(i)){if(l(t[i])&&l(n[i])){if(i==="designSystem")b(n[i],t[i],r,i);else for(const c in t[i])if(!r.has(`${i}/${c}`)){if(i==="methods"&&d(c)){c in n[i]||a.push(c);continue}c in n[i]?k(i,n[i][c])&&!k(i,t[i][c])&&(n[i][c]=t[i][c]):n[i][c]=t[i][c]}}else if(!(i in n)){const c=t[i],f=i==="methods"&&c&&typeof c=="object"?C(c,r,a):c;n[i]=g(f,r,i)}}a.length&&T(e,a)},G=(n,t,o)=>{if(n!==t){const e=S(n);if(u(e)&&e)return e;if(n.key||n.name)return n.key||n.name}return t&&(t.key||t.name)||`sharedLibraries[${o}]`},q=(n,t)=>{if(!t||!t.length)return;const o=P(n.designSystem);for(let e=0;e<t.length;e++){const s=t[e];let r=s,a=F;A(r)&&(a=L(r.ignoreList),r=r.library),l(r)&&N(n,r,a,G(s,r,e))}o.size&&M(n.designSystem,o)};async function W(n,t={}){if((t.provider||"kv")==="api")return _(n,t);const e=await z(n,t);return e||_(n,t)}async function z(n,t={}){const o=t.kvBaseUrl||B,e=t.env||"production",s=`${o}/kv/${encodeURIComponent(n)}?env=${e}`;try{const r=await fetch(s,{method:"GET"});return r.ok&&(await r.json())?.value||null}catch{return null}}async function _(n,t={}){const o=t.apiBaseUrl||E,{key:e}=h(n);try{const s=`${o}/core/projects/libraries/available?search=${encodeURIComponent(e)}&limit=10`,r=await fetch(s,{method:"GET"});if(!r.ok)return null;const a=await r.json(),c=(a?.items||a?.data||(Array.isArray(a)?a:[])).find(j=>{const{key:R}=h(j?.key);return R.toLowerCase()===e.toLowerCase()});if(!c?.id&&!c?._id)return null;const f=c.id||c._id,I=`${o}/core/projects/${encodeURIComponent(f)}/data?branch=main`,p=await fetch(I,{method:"GET"});if(!p.ok)return null;const y=await p.json();return y?.data||y||null}catch{return null}}async function v(n,t={}){if(l(n))return n;if(u(n)){const{full:o}=h(n);if(!o)return null;try{const e=await W(o,t);return e||console.warn(`[smbls] Shared library "${o}" not found`),e}catch(e){return console.warn(`[smbls] Failed to fetch shared library "${o}":`,e.message),null}}return null}async function H(n,t={}){return!n||!n.length?[]:(await Promise.all(n.map(async e=>{if(A(e)){const s=await v(S(e),t);return s?{library:s,ignoreList:e.ignoreList}:null}return v(e,t)}))).filter(Boolean)}export{V as __resetBuiltinMethodWarnings,b as deepDefaults,W as fetchLibraryData,A as isWrappedLibrary,q as mergeSharedLibraries,L as normalizeIgnoreList,h as normalizeLibraryKey,H as resolveSharedLibraries,x as sharedLibrary};
@@ -1,3 +1,3 @@
1
- const N=(e,r)=>{for(const t of r)if(e.includes(t))return!0;return!1},O=(e,r)=>{const t=new RegExp(`[${r.join("\\")}]`,"g");return e.replace(t,"")},L={2:/{{\s*((?:\.\.\/)*)([\w\d.]+)\s*(?:\|\s*([\w\d.]+))?\s*}}/g,3:/{{{(\s*(?:\.\.\/)*)([\w\d.]+)\s*(?:\|\s*([\w\d.]+))?\s*}}}/g},x=(e,r)=>r.split(".").reduce((t,a)=>t?.[a],e);function I(e,r,t={}){const{bracketsLength:a=2}=t,f=a===3?"{{{":"{{";if(!e.includes(f))return e;const m=L[a],c=r||this.state||{},i=this;return e.replace(m,(u,d,n,l)=>{const g=n.trim();if(l){const s=i?.context,o=s?.functions?.[l]||s?.utils?.[l]||s?.methods?.[l]||s?.snippets?.[l]||i?.[l];if(o&&typeof o=="function")try{return String(o.call(i,g)??"")}catch{return""}return""}if(d){const s=(d.match(/\.\.\//g)||[]).length;let o=c;for(let p=0;p<s;p++){if(!o||!o.parent)return"";o=o.parent}if(g==="parent")return String(o.value??"");const h=x(o,g);return h!=null&&typeof h=="object"?"":String(h??"")}else{const s=x(c,g);if(s!=null&&typeof s!="object")return String(s);const o=i?.context?.polyglot;if(o?.translations){const h=c?.root?.lang||c?.lang||i?.context?.state?.lang||o.defaultLang||"en",p=o.translations[h];if(p){const C=x(p,g);if(C!=null&&typeof C!="object")return String(C);for(const w in p){const b=p[w];if(b&&typeof b=="object"&&!Array.isArray(b)){const S=x(b,g);if(S!=null&&typeof S!="object")return String(S)}}}const y=x(o.translations,g);if(y!=null&&typeof y!="object")return String(y)}return""}})}const R=e=>`${e.charAt(0).toLowerCase()}${e.slice(1)}`,k=(e,r)=>{const t=e.split(`
2
- `);let a=-1,f=-1,m=-1,c=-1;const i=new RegExp(`\\b${r}\\b\\s*:\\s*`);let u=0,d=!1;for(let n=0;n<t.length;n++)if(i.test(t[n])&&!d){if(d=!0,a=n+1,m=t[n].indexOf(r)+1,t[n].includes("{}")){f=a,c=t[n].indexOf("{}")+3;break}const l=t[n].slice(m+r.length);if(l.includes("{")||l.includes("["))u=1;else{f=n+1,c=t[n].length+1;break}}else if(d&&(u+=(t[n].match(/{/g)||[]).length,u+=(t[n].match(/\[/g)||[]).length,u-=(t[n].match(/}/g)||[]).length,u-=(t[n].match(/]/g)||[]).length,u===0)){f=n+1,c=t[n].lastIndexOf("}")!==-1?t[n].lastIndexOf("}")+2:t[n].length+1;break}return{startColumn:m,endColumn:c,startLineNumber:a,endLineNumber:f}},j=/\\([0-7]{1,3})/g,E=e=>e.replace(j,(r,t)=>String.fromCharCode(parseInt(t,8))),$=e=>e.replace(/\n/g,"/////n").replace(/`/g,"/////tilde").replace(/\$/g,"/////dlrsgn"),F=e=>e.replace(/\/\/\/\/\/n/g,`
3
- `).replace(/\/\/\/\/\/tilde/g,"`").replace(/\/\/\/\/\/dlrsgn/g,"$"),A=/[^a-zA-Z0-9\s]/g,_=e=>e.replace(A,r=>"%"+r.charCodeAt(0).toString(16).toUpperCase()),v=e=>e.replace(/%[0-9A-Fa-f]{2}/g,r=>String.fromCharCode(parseInt(r.slice(1),16)));export{v as customDecodeURIComponent,_ as customEncodeURIComponent,F as decodeNewlines,$ as encodeNewlines,k as findKeyPosition,R as lowercaseFirstLetter,I as replaceLiteralsWithObjectFields,E as replaceOctalEscapeSequences,N as stringIncludesAny,O as trimStringFromSymbols};
1
+ const N=(t,r)=>{for(const e of r)if(t.includes(e))return!0;return!1},O=(t,r)=>{const e=new RegExp(`[${r.join("\\")}]`,"g");return t.replace(e,"")},w={2:/{{\s*((?:\.\.\/)*)([\w\d.]+)\s*(?:\|\s*([\w\d.]+))?\s*}}/g,3:/{{{(\s*(?:\.\.\/)*)([\w\d.]+)\s*(?:\|\s*([\w\d.]+))?\s*}}}/g},x=(t,r)=>r.split(".").reduce((e,s)=>e?.[s],t),A=(t,r)=>{if(!t||!t.__ref)return!0;let e=t;for(;e;){if(e.allowRuntimeTemplates===!0)return!0;const s=e.__ref&&e.__ref.__authoredTemplates;if(s&&s.has(r))return!0;e=e.parent}return!1};function R(t,r,e={}){const{bracketsLength:s=2}=e,f=s===3?"{{{":"{{";if(!t.includes(f)||!A(this,t))return t;const m=w[s],l=r||this.state||{},i=this;return t.replace(m,(u,d,n,a)=>{const p=n.trim();if(a){const c=i?.context,o=c?.functions?.[a]||c?.utils?.[a]||c?.methods?.[a]||c?.snippets?.[a]||i?.[a];if(o&&typeof o=="function")try{return String(o.call(i,p)??"")}catch{return""}return""}if(d){const c=(d.match(/\.\.\//g)||[]).length;let o=l;for(let g=0;g<c;g++){if(!o||!o.parent)return"";o=o.parent}if(p==="parent")return String(o.value??"");const h=x(o,p);return h!=null&&typeof h=="object"?"":String(h??"")}else{const c=x(l,p);if(c!=null&&typeof c!="object")return String(c);const o=i?.context?.polyglot;if(o?.translations){const h=l?.root?.lang||l?.lang||i?.context?.state?.lang||o.defaultLang||"en",g=o.translations[h];if(g){const C=x(g,p);if(C!=null&&typeof C!="object")return String(C);for(const _ in g){const b=g[_];if(b&&typeof b=="object"&&!Array.isArray(b)){const S=x(b,p);if(S!=null&&typeof S!="object")return String(S)}}}const y=x(o.translations,p);if(y!=null&&typeof y!="object")return String(y)}return""}})}const I=t=>`${t.charAt(0).toLowerCase()}${t.slice(1)}`,k=(t,r)=>{const e=t.split(`
2
+ `);let s=-1,f=-1,m=-1,l=-1;const i=new RegExp(`\\b${r}\\b\\s*:\\s*`);let u=0,d=!1;for(let n=0;n<e.length;n++)if(i.test(e[n])&&!d){if(d=!0,s=n+1,m=e[n].indexOf(r)+1,e[n].includes("{}")){f=s,l=e[n].indexOf("{}")+3;break}const a=e[n].slice(m+r.length);if(a.includes("{")||a.includes("["))u=1;else{f=n+1,l=e[n].length+1;break}}else if(d&&(u+=(e[n].match(/{/g)||[]).length,u+=(e[n].match(/\[/g)||[]).length,u-=(e[n].match(/}/g)||[]).length,u-=(e[n].match(/]/g)||[]).length,u===0)){f=n+1,l=e[n].lastIndexOf("}")!==-1?e[n].lastIndexOf("}")+2:e[n].length+1;break}return{startColumn:m,endColumn:l,startLineNumber:s,endLineNumber:f}},L=/\\([0-7]{1,3})/g,E=t=>t.replace(L,(r,e)=>String.fromCharCode(parseInt(e,8))),$=t=>t.replace(/\n/g,"/////n").replace(/`/g,"/////tilde").replace(/\$/g,"/////dlrsgn"),F=t=>t.replace(/\/\/\/\/\/n/g,`
3
+ `).replace(/\/\/\/\/\/tilde/g,"`").replace(/\/\/\/\/\/dlrsgn/g,"$"),j=/[^a-zA-Z0-9\s]/g,T=t=>t.replace(j,r=>"%"+r.charCodeAt(0).toString(16).toUpperCase()),v=t=>t.replace(/%[0-9A-Fa-f]{2}/g,r=>String.fromCharCode(parseInt(r.slice(1),16)));export{v as customDecodeURIComponent,T as customEncodeURIComponent,F as decodeNewlines,$ as encodeNewlines,k as findKeyPosition,I as lowercaseFirstLetter,R as replaceLiteralsWithObjectFields,E as replaceOctalEscapeSequences,N as stringIncludesAny,O as trimStringFromSymbols};
package/keys.js CHANGED
@@ -148,6 +148,43 @@ export const METHODS = new Set([
148
148
  'preventContentRecreate'
149
149
  ])
150
150
 
151
+ // Is `name` a built-in element method (or a reserved element key)? Every method
152
+ // on @symbo.ls/element's prototype is in METHODS — locked by
153
+ // element/__tests__/libraryMethodsCannotShadowBuiltins.test.js.
154
+ export const isBuiltinElementMethod = (name) => METHODS.has(name)
155
+
156
+ // The built-in element methods a SHARED LIBRARY may replace — the OVERRIDE
157
+ // POINTS (FW-LIBRARY-METHODS-CANNOT-SHADOW-BUILTIN-ELEMENT-METHODS-1). They are
158
+ // observability hooks, not element semantics: the element's own tree, state
159
+ // and rendering never depend on what they do, so a library's version cannot
160
+ // break the element the way a stale getRoot / getRootState copy did — and
161
+ // apps link libraries precisely to route these outputs. Every other built-in
162
+ // is protected: a library copy is refused, the built-in wins. The app's OWN
163
+ // methods win over both, as for any method.
164
+ export const ELEMENT_METHOD_OVERRIDE_POINTS = new Set([
165
+ // Message logging. The built-in is a debug dump — element `log(...keys)`
166
+ // prints `{ key: el[key] }`; a library's `log(...params)` prints its
167
+ // messages, which is what its own methods call it for.
168
+ 'log',
169
+ // Warning channel. The built-in prints only in test/development; a library
170
+ // routes warnings in every environment (console, telemetry) and continues.
171
+ 'warn',
172
+ // Error channel. The built-in THROWS in test/development and is silent in
173
+ // production; a library reports and continues (`return this.error('…')` is
174
+ // an early exit that must not become an exception), with its own hooks
175
+ // (`onError`, `verbose`, `debugger` options).
176
+ 'error',
177
+ // Debug inspector for an element — a development aid a library may style
178
+ // or redirect.
179
+ 'verbose'
180
+ ])
181
+
182
+ // A built-in element method that a library can NOT replace: METHODS minus the
183
+ // override points. The single test the shared-library merge and the element's
184
+ // fold / `el.call` paths use.
185
+ export const isProtectedElementMethod = (name) =>
186
+ METHODS.has(name) && !ELEMENT_METHOD_OVERRIDE_POINTS.has(name)
187
+
151
188
  export const METHODS_EXL = new Set([
152
189
  'node', 'context', 'extends', '__element', '__ref',
153
190
  ...METHODS,
package/methods.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { triggerEventOn } from './triggerEvent.js'
4
4
  import { DOMQ_PROPERTIES, METHODS, PARSED_DOMQ_PROPERTIES } from './keys.js'
5
5
  import { isDefined, isFunction, isObject, isObjectLike } from './types.js'
6
- import { deepClone } from './object.js'
6
+ import { deepClone, isFailedRevival } from './object.js'
7
7
  import { isProduction } from './env.js'
8
8
  import { removeValueFromArray } from './array.js'
9
9
  import { OPTIONS } from './cache.js'
@@ -214,6 +214,25 @@ export const setActiveRootState = (state) => {
214
214
  _activeRootState = state || null
215
215
  }
216
216
 
217
+ /**
218
+ * `prepareContext` registers the PREPARED root state — a plain object — before
219
+ * the app root element exists. The root element then builds its OWN reactive
220
+ * store from that object (@symbo.ls/element createElementState), so the
221
+ * registered object is only a boot-time copy: it names no element, never sees
222
+ * a write and has no store methods. Every `getRootState()` fallback read went
223
+ * stale, and `getRoot()`'s fallback (`getRootState()?.__element`) could never
224
+ * resolve the app for an element whose `__ref` was cleared by `remove()`
225
+ * (FW-ROOT-STATE-REGISTERED-AS-A-DETACHED-PLAIN-COPY-1).
226
+ *
227
+ * @symbo.ls/element calls this when it builds a store from `source`: if the
228
+ * registry holds exactly `source`, it now holds the live `store`. Gated on
229
+ * identity, so an unrelated element tree created later can never take the
230
+ * registry over.
231
+ */
232
+ export const promoteActiveRootState = (source, store) => {
233
+ if (source && store && _activeRootState === source) _activeRootState = store
234
+ }
235
+
217
236
  const readFallbackState = () => {
218
237
  // The framework-managed registry (set by prepareContext during
219
238
  // create(...)) is the single source of truth for the active root
@@ -472,6 +491,10 @@ export function call (fnKey, ...args) {
472
491
  context.snippets?.[fnKey]
473
492
  )
474
493
  if (!fn) return
494
+ // A lazily revived function whose source does not compile stands for the
495
+ // string eager revival left (object.js "LAZY REVIVAL"), which this helper
496
+ // could not call either: undefined, as before.
497
+ if (isFailedRevival(fn)) return
475
498
  try {
476
499
  const result = fn.call(this, ...args)
477
500
  if (result && typeof result.then === 'function') {
package/object.js CHANGED
@@ -387,16 +387,32 @@ const RE_MODULE_PREFIX = /^(export|import)\s/
387
387
  export const hasFunction = (str) => {
388
388
  if (!str) return false
389
389
 
390
- const trimmed = str.trim().replace(/\n\s*/g, ' ').trim()
390
+ // SMBLS-BOOT-LAZY-EVAL-1: this runs on EVERY string of a project context
391
+ // at boot (Pulse: ~26k strings, 2 MB), so it must reject a non-function
392
+ // string cheaply. Both steps below are exact — the result is identical to
393
+ // the previous form (`str.trim().replace(/\n\s*/g, ' ').trim()` then every
394
+ // pattern), pinned by __tests__/hasFunctionEquivalence.test.js:
395
+ // • no newline collapsing: every pattern reads `\s`, `[^)]`, `[^}]`,
396
+ // `[^=]` or `[\s\S]` where a newline run and a space both match, and
397
+ // nothing else looks at whitespace;
398
+ // • the pre-filter: every pattern needs `=>`, or a `function` / `async`
399
+ // / `(` prefix, or a `(` plus a closing `}` (method shorthand) — a
400
+ // string with none of those can match none of them.
401
+ const trimmed = str.trim()
391
402
 
392
403
  if (trimmed === '' || trimmed === '{}' || trimmed === '[]') return false
404
+ const hasArrow = trimmed.includes('=>')
405
+ if (!hasArrow &&
406
+ trimmed.charCodeAt(0) !== 40 && // '(' — `(function …`
407
+ !trimmed.startsWith('function') &&
408
+ !trimmed.startsWith('async') &&
409
+ !(trimmed.charCodeAt(trimmed.length - 1) === 125 && trimmed.includes('('))) return false
393
410
  if (RE_MODULE_PREFIX.test(trimmed)) return false
394
411
 
395
412
  const isFn = FN_PATTERNS.some((pattern) => pattern.test(trimmed))
396
413
  if (!isFn) return false
397
414
 
398
415
  const firstChar = trimmed.charCodeAt(0)
399
- const hasArrow = trimmed.includes('=>')
400
416
  // '{' = 123, '[' = 91
401
417
  if (firstChar === 123 && !hasArrow) return false // object literal
402
418
  if (firstChar === 91) return false // array literal
@@ -518,8 +534,12 @@ const stripTopLevelDeclaration = (src, name) => {
518
534
  // the structural walker used inline. Returns either the eval'd value or
519
535
  // the original string (with a console.warn) so callers can assign the
520
536
  // result directly. `label` is used in warning messages so the surface
521
- // of the failure (object key vs array index) is identifiable.
522
- const _destringifyFnString = (str, label, opts) => {
537
+ // of the failure (object key vs array index) is identifiable. `lazy` only
538
+ // changes the warning's description of the outcome: a lazily revived
539
+ // function (see LAZY REVIVAL below) compiles on its first use, and when the
540
+ // source does not compile it stands for the string an eager revival leaves
541
+ // in place.
542
+ const _destringifyFnString = (str, label, opts, lazy) => {
523
543
  // Module SOURCE files are stored as plain strings on project data
524
544
  // (`code` fields driving CodePreview etc. — they start with
525
545
  // `export …` / `import …`). They are display text, not function
@@ -594,7 +614,10 @@ const _destringifyFnString = (str, label, opts) => {
594
614
  if (typeof console !== 'undefined' && console.warn) {
595
615
  console.warn(
596
616
  `[smbls] deepDestringifyFunctions: eval failed on ${label} — ` +
597
- 'value will be left as a string. Reason: ' + msg + '.\n' +
617
+ (lazy
618
+ ? 'revived lazily, so its source compiled on this first use; it acts as the string it replaces ' +
619
+ '(a call returns the source text, el.call treats it as not callable). Reason: '
620
+ : 'value will be left as a string. Reason: ') + msg + '.\n' +
598
621
  'First 200 chars of source: ' + String(str).slice(0, 200)
599
622
  )
600
623
  }
@@ -602,12 +625,350 @@ const _destringifyFnString = (str, label, opts) => {
602
625
  }
603
626
  }
604
627
 
628
+ // ─────────────────────────────────────────────────────────────────────────
629
+ // LAZY REVIVAL — SMBLS-BOOT-LAZY-EVAL-1
630
+ //
631
+ // Measured on anseta/pulse 1.31.1 (runtime 3.14.808, Lighthouse mobile):
632
+ // the page context carries 3,735 function strings (1.82 MB of source), and
633
+ // the eager walk ran one indirect eval per string before the first render —
634
+ // the largest self time of the whole boot (561 ms unthrottled, ~2.2 s at
635
+ // 4x CPU). A page calls a small share of them: one route's props and a few
636
+ // dozen registered functions.
637
+ //
638
+ // `deepDestringifyFunctions(obj, dest, { lazy: true })` keeps each source
639
+ // and installs a WRAPPER that compiles it on the first call — through the
640
+ // SAME `opts.window.eval` and the SAME recovery passes (`_destringifyFnString`
641
+ // above) the eager walk uses — caches the result and calls straight through
642
+ // from then on. Opt-in: the default stays eager, because tooling reads the
643
+ // eager result (frank's toFS prints a string that failed to compile as a
644
+ // quoted literal, helmet tests for "still a string", the workspace re-realm
645
+ // pass reads `Function.prototype.toString`). The runtime boot paths opt in:
646
+ // the published/runner page scripts and prepareContext's rehydrate.
647
+ //
648
+ // What a wrapper preserves (each pinned in __tests__/deepDestringifyLazy.test.js):
649
+ // • identity — the wrapper IS the function for its whole life. It never
650
+ // swaps itself out after compiling, so WeakMap keys, listener dedupe and
651
+ // `===` stay stable. Two slots with equal source get two wrappers, as
652
+ // two evals gave two functions.
653
+ // • `this`, every argument, the return value (an async source still
654
+ // returns its promise), and `new` for a plain `function` source
655
+ // (`Reflect.construct` with the wrapper as new.target, so instances are
656
+ // `instanceof` the wrapper the outside world holds).
657
+ // • `typeof` 'function', `.call`/`.apply`/`.bind`.
658
+ // • `.toString()` — the ORIGINAL source (trimmed, as an eval'd function
659
+ // expression reports it), without compiling. frank, the editor, SSR and
660
+ // inspectors re-serialize through it.
661
+ // • `.length` — the compiled function's arity, read through an accessor
662
+ // that compiles on demand (only the dev-only stashed-callback advisory
663
+ // in element/create.js and `Function.prototype.bind` read it).
664
+ // • `.name` — what the eager eval would report: the declared name of a
665
+ // `function` or method source, '' for an arrow or anonymous function.
666
+ // • no `.prototype`, not constructible, for arrow / async / method
667
+ // sources — a method-shorthand wrapper, like the eval'd value; an
668
+ // async source's wrapper inherits %AsyncFunction.prototype%, so
669
+ // `instanceof`, `.constructor` and the toStringTag say AsyncFunction.
670
+ //
671
+ // What cannot be preserved: `Function.prototype.toString.call(wrapper)`
672
+ // reads the wrapper's own text, not the stored source — no function object
673
+ // can answer it without compiling. `getFunctionSource(fn)` below answers it
674
+ // for any function; the one in-repo caller (element's dev advisory) uses it.
675
+ // Two more, kept on purpose (PR #187 review N2, N3):
676
+ // • `.caller` / `.arguments` on the wrapper of a plain `function` source
677
+ // throw (the wrapper is strict code). A sloppy wrapper would answer them,
678
+ // but it would also coerce `this` (undefined → globalThis, primitives →
679
+ // objects) before a strict source sees it; `this` is the contract.
680
+ // • every call runs one extra frame, so the deepest recursion through a
681
+ // registry reference roughly halves (probe: 8,959 → 4,607 frames).
682
+ // Swapping the slot to the compiled function after the first call would
683
+ // remove the frame but split identity (a listener added with the wrapper
684
+ // could no longer be removed with the slot's value).
685
+ //
686
+ // Only an UNAMBIGUOUS function expression is revived lazily (see
687
+ // `lazyStateFor`): an arrow, a plain/async `function`, or a method
688
+ // shorthand, whose head the regexes below prove. Everything else `hasFunction`
689
+ // accepts — prose that starts with "function", a generator (its `.prototype`
690
+ // is a generator prototype), a named function that refers to itself by name
691
+ // (its self-binding would be the compiled function, not the wrapper), a
692
+ // `(function…)` wrapped form — keeps the eager path exactly as before, string
693
+ // fallback and boot-time warning included.
694
+ //
695
+ // A lazily revived source that fails to compile warns with today's
696
+ // diagnostic (prefix, reason, first 200 chars) on its FIRST USE instead of
697
+ // at boot, and from then on stands for the STRING eager revival leaves in
698
+ // place (PR #187 review S4): every call returns that string, so a prop DOMQL
699
+ // evaluates by calling it (`text`, `if`, `hide`, attributes, CSS, custom
700
+ // props, `state`) gets exactly the value the string gave — a broken `if`
701
+ // stays truthy, prose in `text` still shows — and `el.call` / utils `call`
702
+ // ask `isFailedRevival` first and treat it as not callable, exactly as they
703
+ // treated the string (undefined, and el.call's "resolves to a non-function
704
+ // (string)" error). What remains different: `typeof` is 'function', and a
705
+ // DIRECT call from other code returns the string where calling a string
706
+ // threw a TypeError.
707
+ //
708
+ // `{ lazy: 'context' }` is the page scripts' mode for a whole project
709
+ // context: only the CODE buckets (LAZY_CONTEXT_CODE_KEYS) revive lazily;
710
+ // data — state, designSystem, polyglot, files, schema, config values —
711
+ // keeps eager revival, so a function-looking data string that does not
712
+ // compile stays the string it is, for code that READS it as a value.
713
+ // `{ lazy: true }` is lazy everywhere: the bucket-level callers
714
+ // (prepareContext's rehydrate of components, pages, functions, …).
715
+ //
716
+ // Identical sources are NOT shared. It would be safe — the compiled
717
+ // function never leaves its wrapper, and the self-referencing shapes that
718
+ // could observe it keep the eager path — but it does not pay: of the 388
719
+ // compiles the first render of Pulse /home triggers, 82 repeat a source
720
+ // (~6 ms of eval), against a per-realm cache every wrapper would consult.
721
+ // ─────────────────────────────────────────────────────────────────────────
722
+
723
+ const LAZY_KIND_PLAIN = 1 // `function` — constructible, has its own `.prototype`
724
+ const LAZY_KIND_CALLABLE = 2 // arrow / async function / method — neither
725
+
726
+ // wrapper → { src, name, kind, async, fn, value, key, index, opts }
727
+ // `fn` is undefined until the first compile, then the compiled function, or
728
+ // null when the source did not compile to one — `value` is then what eager
729
+ // revival leaves in the slot (the source string).
730
+ const LAZY_FN_STATE = new WeakMap()
731
+
732
+ // Every head below must PROVE its shape up to the body's opening brace or the
733
+ // arrow. Parameters are accepted without strings, template literals or slashes
734
+ // (comments, regex literals, division) and with at most one level of nested
735
+ // parens; a head these cannot prove falls back to the eager path — it is
736
+ // never misread.
737
+ const RE_LAZY_FUNCTION_HEAD = /^(async\s+)?function(?![\w$])\s*(\*)?\s*([A-Za-z_$][\w$]*)?\s*\((?:[^()'"`/]|\([^()'"`/]*\))*\)\s*\{/
738
+ const RE_LAZY_ARROW_IDENT_HEAD = /^(async\s+)?[A-Za-z_$][\w$]*\s*=>/
739
+ const RE_LAZY_ARROW_PARENS_HEAD = /^(async\s*)?\((?:[^()'"`/]|\([^()'"`/]*\))*\)\s*=>/
740
+ const RE_LAZY_METHOD_HEAD = /^(async\s+)?([A-Za-z_$][\w$]*)\s*\((?:[^()'"`/]|\([^()'"`/]*\))*\)\s*\{/
741
+
742
+ // A method-shorthand head named like a keyword is prose or a statement
743
+ // (`if (x) { … }`, `for (…) { … }`), never a revivable method.
744
+ const LAZY_NON_METHOD_NAMES = new Set([
745
+ 'if', 'for', 'while', 'switch', 'catch', 'with', 'do', 'return', 'typeof',
746
+ 'void', 'delete', 'new', 'in', 'of', 'instanceof', 'function', 'class',
747
+ 'const', 'let', 'var', 'await', 'yield', 'import', 'export', 'try', 'else',
748
+ 'throw', 'case', 'default', 'break', 'continue', 'debugger', 'finally',
749
+ 'super', 'this', 'null', 'true', 'false', 'async', 'get', 'set', 'static'
750
+ ])
751
+
752
+ const _isIdentCharCode = (c) =>
753
+ (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 36 || c === 95
754
+
755
+ // `name` appears in `src` at or after `from` as a whole identifier token.
756
+ // Conservative on purpose (a property named like the function also counts):
757
+ // a false hit only means the eager path.
758
+ const _mentionsIdentifier = (src, name, from) => {
759
+ let i = src.indexOf(name, from)
760
+ while (i !== -1) {
761
+ const end = i + name.length
762
+ if ((i === 0 || !_isIdentCharCode(src.charCodeAt(i - 1))) &&
763
+ (end >= src.length || !_isIdentCharCode(src.charCodeAt(end)))) return true
764
+ i = src.indexOf(name, end)
765
+ }
766
+ return false
767
+ }
768
+
769
+ const _lastNonSpaceCharCode = (s) => {
770
+ let i = s.length - 1
771
+ while (i >= 0 && s.charCodeAt(i) <= 32) i--
772
+ return i < 0 ? -1 : s.charCodeAt(i)
773
+ }
774
+
775
+ // Classify one source string for lazy revival: a fresh state object, or null
776
+ // when the eager path must revive it (see the header above for each case).
777
+ const lazyStateFor = (src) => {
778
+ let s = src
779
+ let start = 0
780
+ while (start < s.length && s.charCodeAt(start) <= 32) start++
781
+ if (start) s = s.slice(start)
782
+
783
+ let m = RE_LAZY_FUNCTION_HEAD.exec(s)
784
+ if (m) {
785
+ if (m[2]) return null // generator
786
+ if (_lastNonSpaceCharCode(s) !== 125) return null // must end with its body's `}`
787
+ const name = m[3] || ''
788
+ if (name && _mentionsIdentifier(s, name, m[0].indexOf('('))) return null // self-reference (params or body)
789
+ if (s.indexOf('arguments.callee') !== -1) return null
790
+ return { src, name, kind: m[1] ? LAZY_KIND_CALLABLE : LAZY_KIND_PLAIN, async: !!m[1], fn: undefined }
791
+ }
792
+ m = RE_LAZY_ARROW_IDENT_HEAD.exec(s) || RE_LAZY_ARROW_PARENS_HEAD.exec(s)
793
+ if (m) {
794
+ return { src, name: '', kind: LAZY_KIND_CALLABLE, async: !!m[1], fn: undefined }
795
+ }
796
+ m = RE_LAZY_METHOD_HEAD.exec(s)
797
+ if (m && !LAZY_NON_METHOD_NAMES.has(m[2]) && _lastNonSpaceCharCode(s) === 125) {
798
+ if (s.indexOf('arguments.callee') !== -1) return null
799
+ return { src, name: m[2], kind: LAZY_KIND_CALLABLE, async: !!m[1], fn: undefined }
800
+ }
801
+ return null
802
+ }
803
+
804
+ const _lazyLabel = (state) => state.index === -1
805
+ ? `"${state.key}"`
806
+ : `array index ${state.index} (prop "${state.key}")`
807
+
808
+ const compileLazy = (state) => {
809
+ const out = _destringifyFnString(state.src, _lazyLabel(state), state.opts, true)
810
+ let fn = null
811
+ if (typeof out === 'function') {
812
+ fn = out
813
+ } else {
814
+ // Not a function: the value eager revival leaves in the slot — the
815
+ // source string when it did not compile (`_destringifyFnString` has
816
+ // warned), or, unreachable for a head the regexes proved, whatever the
817
+ // expression evaluated to. The wrapper stands for it from now on.
818
+ state.value = out
819
+ if (typeof out !== 'string' && typeof console !== 'undefined' && console.warn) {
820
+ console.warn(
821
+ `[smbls] deepDestringifyFunctions: ${_lazyLabel(state)} did not evaluate to a function — ` +
822
+ 'the lazily revived function returns that value.\n' +
823
+ 'First 200 chars of source: ' + String(state.src).slice(0, 200)
824
+ )
825
+ }
826
+ }
827
+ state.fn = fn
828
+ state.opts = null // the eval handle is no longer needed
829
+ return fn
830
+ }
831
+
832
+ // Kept out of the wrappers' own text so `Function.prototype.toString.call`
833
+ // on a wrapper names no global at all (the workspace re-realm gate reads it).
834
+ const _constructLazy = (fn, args, newTarget) => Reflect.construct(fn, args, newTarget)
835
+
836
+ function lazyFunctionToString () {
837
+ const state = LAZY_FN_STATE.get(this)
838
+ return state ? state.src.trim() : Function.prototype.toString.call(this)
839
+ }
840
+
841
+ // Shared by every wrapper — one defineProperties call, no per-wrapper
842
+ // descriptor objects.
843
+ const LAZY_FN_DESCRIPTORS = {
844
+ length: {
845
+ get () {
846
+ const state = LAZY_FN_STATE.get(this)
847
+ if (!state) return 0
848
+ const fn = state.fn === undefined ? compileLazy(state) : state.fn
849
+ return fn ? fn.length : 0
850
+ },
851
+ configurable: true
852
+ },
853
+ name: {
854
+ get () {
855
+ const state = LAZY_FN_STATE.get(this)
856
+ return state ? state.name : ''
857
+ },
858
+ configurable: true
859
+ },
860
+ toString: { value: lazyFunctionToString, writable: true, configurable: true }
861
+ }
862
+
863
+ // An async source's wrapper inherits from %AsyncFunction.prototype% (review
864
+ // N1): `Object.prototype.toString` reports '[object AsyncFunction]',
865
+ // `.constructor` is AsyncFunction and `instanceof` holds — while the wrapper
866
+ // itself stays a plain synchronous call that hands back the inner promise
867
+ // (an `async` wrapper would add microtask ticks to every call).
868
+ const ASYNC_FUNCTION_PROTOTYPE = Object.getPrototypeOf(async function () {})
869
+
870
+ const makeLazyFunction = (state) => {
871
+ let wrapper
872
+ if (state.kind === LAZY_KIND_PLAIN) {
873
+ wrapper = function () {
874
+ const fn = state.fn === undefined ? compileLazy(state) : state.fn
875
+ if (fn === null) return state.value
876
+ return new.target
877
+ ? _constructLazy(fn, arguments, new.target)
878
+ : fn.apply(this, arguments)
879
+ }
880
+ } else {
881
+ // A method shorthand: receives `this`, has no `.prototype`, and throws
882
+ // on `new` — the same as the arrow / async / method it stands for.
883
+ wrapper = ({
884
+ lazy () {
885
+ const fn = state.fn === undefined ? compileLazy(state) : state.fn
886
+ if (fn === null) return state.value
887
+ return fn.apply(this, arguments)
888
+ }
889
+ }).lazy
890
+ }
891
+ LAZY_FN_STATE.set(wrapper, state)
892
+ Object.defineProperties(wrapper, LAZY_FN_DESCRIPTORS)
893
+ if (state.async) Object.setPrototypeOf(wrapper, ASYNC_FUNCTION_PROTOTYPE)
894
+ return wrapper
895
+ }
896
+
897
+ // Revive one function string lazily when its head allows, eagerly otherwise.
898
+ const _reviveLazily = (str, key, index, opts) => {
899
+ const state = lazyStateFor(str)
900
+ if (!state) {
901
+ return _destringifyFnString(
902
+ str,
903
+ index === -1 ? `"${key}"` : `array index ${index} (prop "${key}")`,
904
+ opts
905
+ )
906
+ }
907
+ state.key = key
908
+ state.index = index
909
+ state.opts = opts
910
+ return makeLazyFunction(state)
911
+ }
912
+
913
+ /**
914
+ * True for a function `deepDestringifyFunctions(…, { lazy: true })` revived
915
+ * lazily (compiled or not yet).
916
+ */
917
+ export const isLazyFunction = (fn) => typeof fn === 'function' && LAZY_FN_STATE.has(fn)
918
+
919
+ /**
920
+ * The source text of any function: the stored source of a lazily revived
921
+ * one (without compiling it), `Function.prototype.toString` for the rest.
922
+ * Use this instead of `Function.prototype.toString.call(fn)`, which reads a
923
+ * lazy wrapper's own text.
924
+ */
925
+ export const getFunctionSource = (fn) => {
926
+ const state = LAZY_FN_STATE.get(fn)
927
+ return state ? state.src.trim() : Function.prototype.toString.call(fn)
928
+ }
929
+
930
+ /**
931
+ * True when `fn` is a lazily revived function whose source does not compile
932
+ * — it stands for the string eager revival would have left, so a caller that
933
+ * treated that string as "not callable" (`el.call`, utils `call`) must treat
934
+ * `fn` the same. Compiles `fn` if it has not run yet (the caller was about to
935
+ * call it). False for every other value.
936
+ */
937
+ export const isFailedRevival = (fn) => {
938
+ const state = typeof fn === 'function' ? LAZY_FN_STATE.get(fn) : undefined
939
+ if (!state) return false
940
+ return (state.fn === undefined ? compileLazy(state) : state.fn) === null
941
+ }
942
+
943
+ // The CODE buckets of a project context — `{ lazy: 'context' }` revives
944
+ // lazily only under these top-level keys (see LAZY REVIVAL above).
945
+ const LAZY_CONTEXT_CODE_KEYS = new Set([
946
+ 'components', 'pages', 'functions', 'methods', 'snippets', 'app', 'globalScope'
947
+ ])
948
+
949
+ const DEFAULT_DESTRINGIFY_OPTS = { window: { eval: __globalEval } }
950
+
605
951
  export const deepDestringifyFunctions = (
606
952
  obj,
607
953
  destringified = {},
608
- opts = { window: { eval: __globalEval } }
954
+ opts = DEFAULT_DESTRINGIFY_OPTS
609
955
  ) => {
610
956
  if (!obj || typeof obj !== 'object') return destringified
957
+ // `opts.window.eval` is the one field every runtime since 2026-04 reads,
958
+ // so a page script can pass `{ lazy: true, window }` to ANY runtime: an
959
+ // older one ignores `lazy` and evals eagerly through `window.eval` (an
960
+ // indirect, global-scope eval — the same as `__globalEval`). An options
961
+ // object without a usable `window.eval` falls back to the global eval
962
+ // instead of throwing on the first function string.
963
+ if (!opts || !opts.window || typeof opts.window.eval !== 'function') {
964
+ opts = { ...opts, window: { eval: __globalEval } }
965
+ }
966
+ // `lazy: true` — lazy everywhere. `lazy: 'context'` — `obj` is a project
967
+ // context: lazy only below its code buckets, eager for everything else.
968
+ const lazyAll = opts.lazy === true
969
+ const lazyContext = opts.lazy === 'context'
970
+ const lazyBelow = (src, prop, lazyHere) => lazyAll ||
971
+ (lazyContext && (src === obj ? LAZY_CONTEXT_CODE_KEYS.has(prop) : lazyHere))
611
972
 
612
973
  // Iterative walk with explicit stack so deeply-nested project data
613
974
  // (a frank-readback of a complex DOMQL tree, for instance) doesn't
@@ -615,25 +976,30 @@ export const deepDestringifyFunctions = (
615
976
  // form did. Stack frame: [src, dest] where dest is the container
616
977
  // results land in. Array items handled in-place; nested objects /
617
978
  // arrays get a fresh child container pushed onto the stack.
618
- const stack = [[obj, destringified]]
979
+ const stack = [[obj, destringified, lazyAll]]
619
980
  while (stack.length) {
620
- const [src, dest] = stack.pop()
981
+ const [src, dest, lazyHere] = stack.pop()
621
982
  for (const prop in src) {
622
983
  if (!Object.prototype.hasOwnProperty.call(src, prop)) continue
623
984
  const objProp = src[prop]
624
985
  if (isString(objProp)) {
625
986
  if (hasFunction(objProp)) {
626
- dest[prop] = _destringifyFnString(objProp, `"${prop}"`, opts)
987
+ dest[prop] = lazyHere
988
+ ? _reviveLazily(objProp, prop, -1, opts)
989
+ : _destringifyFnString(objProp, `"${prop}"`, opts)
627
990
  } else {
628
991
  dest[prop] = objProp
629
992
  }
630
993
  } else if (isArray(objProp)) {
631
994
  const arr = (dest[prop] = [])
995
+ const lazyItems = lazyBelow(src, prop, lazyHere)
632
996
  for (let i = 0; i < objProp.length; i++) {
633
997
  const arrProp = objProp[i]
634
998
  if (isString(arrProp)) {
635
999
  arr.push(hasFunction(arrProp)
636
- ? _destringifyFnString(arrProp, `array index ${i} (prop "${prop}")`, opts)
1000
+ ? (lazyItems
1001
+ ? _reviveLazily(arrProp, prop, i, opts)
1002
+ : _destringifyFnString(arrProp, `array index ${i} (prop "${prop}")`, opts))
637
1003
  : arrProp)
638
1004
  } else if (isObject(arrProp)) {
639
1005
  const tagged = _rehydrateTagged(arrProp)
@@ -642,7 +1008,7 @@ export const deepDestringifyFunctions = (
642
1008
  } else {
643
1009
  const child = {}
644
1010
  arr.push(child)
645
- stack.push([arrProp, child])
1011
+ stack.push([arrProp, child, lazyItems])
646
1012
  }
647
1013
  } else {
648
1014
  arr.push(arrProp)
@@ -665,7 +1031,7 @@ export const deepDestringifyFunctions = (
665
1031
  const child = (dest[prop] && typeof dest[prop] === 'object' && !isArray(dest[prop]))
666
1032
  ? dest[prop]
667
1033
  : (dest[prop] = {})
668
- stack.push([objProp, child])
1034
+ stack.push([objProp, child, lazyBelow(src, prop, lazyHere)])
669
1035
  } else {
670
1036
  dest[prop] = objProp
671
1037
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/utils",
3
- "version": "3.14.774",
3
+ "version": "3.14.776",
4
4
  "license": "CC-BY-NC-4.0",
5
5
  "type": "module",
6
6
  "module": "./dist/esm/index.js",