@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,6 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  import { isObject, isString } from './types.js'
4
+ import { isProtectedElementMethod } from './keys.js'
5
+ import { isLoudDevRuntime } from './env.js'
4
6
 
5
7
  const DEFAULT_LIBRARY_OWNER = 'system'
6
8
 
@@ -271,7 +273,68 @@ export const isWrappedLibrary = (entry) =>
271
273
  const wrappedLibraryRef = (entry) =>
272
274
  entry.library !== undefined ? entry.library : (entry.key ?? entry.name)
273
275
 
274
- const mergeOneLibrary = (context, sharedLib, ignore) => {
276
+ /**
277
+ * A library's `methods` never replace a PROTECTED built-in element method
278
+ * (FW-LIBRARY-METHODS-CANNOT-SHADOW-BUILTIN-ELEMENT-METHODS-1).
279
+ *
280
+ * `@symbo.ls/element` layers `context.methods` over its built-in element methods
281
+ * (getContextProto) and `el.call(name)` reads `context.methods` first, so a
282
+ * library key named `getRoot` / `getRootState` / … used to replace the built-in
283
+ * on every element of the consuming app, silently — and kept replacing it after
284
+ * the framework fixed the built-in. Measured on a live app: a library's
285
+ * pre-FW-GETROOT `getRoot` copy answered undefined inside installed modules and
286
+ * every module popover was dead with no error.
287
+ *
288
+ * So the merge refuses such a key on both branches and the built-in wins —
289
+ * except for the OVERRIDE POINTS (keys.js ELEMENT_METHOD_OVERRIDE_POINTS: the
290
+ * logging hooks log / warn / error / verbose), which merge like any library
291
+ * method: a library's "log and continue" versions are what its consumers rely
292
+ * on, and no element semantics depend on them. The APP's OWN `methods` are
293
+ * untouched (they were in the bag before any library merged) and win over
294
+ * both. One dev warning per refused name for the page.
295
+ */
296
+ const _warnedBuiltinMethods = new Set()
297
+
298
+ // Test-only: forget which names already warned.
299
+ export const __resetBuiltinMethodWarnings = () => {
300
+ _warnedBuiltinMethods.clear()
301
+ }
302
+
303
+ const warnRefusedBuiltinMethods = (label, names) => {
304
+ const fresh = names.filter((n) => !_warnedBuiltinMethods.has(n))
305
+ if (!fresh.length) return
306
+ for (const n of fresh) _warnedBuiltinMethods.add(n)
307
+ if (!isLoudDevRuntime()) return
308
+ console.warn(
309
+ `[smbls] shared library "${label}" ships ${fresh.map((n) => `methods.${n}`).join(', ')} — ` +
310
+ `named like a built-in element method. Refused: the built-in wins; a library cannot ` +
311
+ `replace an element method on the consuming app. If the app wants this version, ` +
312
+ `declare it in the app's OWN methods.`
313
+ )
314
+ }
315
+
316
+ // The library's `methods` bag without protected built-in names, for the
317
+ // wholesale-copy branch. Same reference when there is nothing to drop (the
318
+ // common case — no allocation, exactly as before); otherwise a copy, because
319
+ // the library's own bag is shared by every consumer and is never edited.
320
+ // A name the consumer ignores (`methods/<name>` in skip) is dropped by
321
+ // copyWithNestedIgnores anyway and is not reported as refused.
322
+ const withoutProtectedMethods = (bag, skip, refused) => {
323
+ let copy = null
324
+ for (const k in bag) {
325
+ if (!isProtectedElementMethod(k)) continue
326
+ if (!skip.has(`methods/${k}`)) refused.push(k)
327
+ if (!copy) {
328
+ copy = {}
329
+ for (const j in bag) {
330
+ if (!isProtectedElementMethod(j)) copy[j] = bag[j]
331
+ }
332
+ }
333
+ }
334
+ return copy || bag
335
+ }
336
+
337
+ const mergeOneLibrary = (context, sharedLib, ignore, label = 'library') => {
275
338
  // A library can declare parts of itself private via `sharedLibIgnore` on
276
339
  // its own context — same path grammar as the consumer-side `ignoreList`
277
340
  // (top-level keys or nested 'components/Header'). A library serving its
@@ -285,6 +348,7 @@ const mergeOneLibrary = (context, sharedLib, ignore) => {
285
348
  const skip = libIgnore.size
286
349
  ? new Set([...ignore, ...libIgnore])
287
350
  : ignore
351
+ const refusedMethods = []
288
352
  for (const key in sharedLib) {
289
353
  if (!Object.prototype.hasOwnProperty.call(sharedLib, key)) continue
290
354
  // merge directives, not library content
@@ -307,6 +371,14 @@ const mergeOneLibrary = (context, sharedLib, ignore) => {
307
371
  } else {
308
372
  for (const k in sharedLib[key]) {
309
373
  if (skip.has(`${key}/${k}`)) continue // nested skip: 'components/Header'
374
+ if (key === 'methods' && isProtectedElementMethod(k)) {
375
+ // The built-in wins. An app-own key of that name is the app's
376
+ // declared override and stays — nothing to report for it. The
377
+ // override points (log / warn / error / verbose) are not protected
378
+ // and fall through to the normal add-missing merge below.
379
+ if (!(k in context[key])) refusedMethods.push(k)
380
+ continue
381
+ }
310
382
  if (!(k in context[key])) {
311
383
  context[key][k] = sharedLib[key][k]
312
384
  } else if (isStubPhantom(key, context[key][k]) && !isStubPhantom(key, sharedLib[key][k])) {
@@ -318,9 +390,28 @@ const mergeOneLibrary = (context, sharedLib, ignore) => {
318
390
  }
319
391
  }
320
392
  } else if (!(key in context)) {
321
- context[key] = copyWithNestedIgnores(sharedLib[key], skip, key)
393
+ // Any object bag, not only a plain one: a native-ESM module namespace
394
+ // (`import * as methods` outside a bundler) has a null prototype and
395
+ // fails isObject, yet lands here and becomes `context.methods` as is.
396
+ const bag = sharedLib[key]
397
+ const value = key === 'methods' && bag && typeof bag === 'object'
398
+ ? withoutProtectedMethods(bag, skip, refusedMethods)
399
+ : bag
400
+ context[key] = copyWithNestedIgnores(value, skip, key)
322
401
  }
323
402
  }
403
+ if (refusedMethods.length) warnRefusedBuiltinMethods(label, refusedMethods)
404
+ }
405
+
406
+ // A readable name for a library in the refusal warning: the wrapped entry's
407
+ // identifier, else the library context's own key/name, else its position.
408
+ const libraryLabel = (entry, lib, index) => {
409
+ if (entry !== lib) {
410
+ const ref = wrappedLibraryRef(entry)
411
+ if (isString(ref) && ref) return ref
412
+ if (entry.key || entry.name) return entry.key || entry.name
413
+ }
414
+ return (lib && (lib.key || lib.name)) || `sharedLibraries[${index}]`
324
415
  }
325
416
 
326
417
  /**
@@ -345,14 +436,15 @@ export const mergeSharedLibraries = (context, libraries) => {
345
436
  // consumer's own bags and the two can no longer be told apart.
346
437
  const ownDefaultFonts = defaultFontClaims(context.designSystem)
347
438
  for (let i = 0; i < libraries.length; i++) {
348
- let sharedLib = libraries[i]
439
+ const entry = libraries[i]
440
+ let sharedLib = entry
349
441
  let ignore = EMPTY_IGNORE
350
442
  if (isWrappedLibrary(sharedLib)) {
351
443
  ignore = normalizeIgnoreList(sharedLib.ignoreList)
352
444
  sharedLib = sharedLib.library
353
445
  }
354
446
  if (!isObject(sharedLib)) continue
355
- mergeOneLibrary(context, sharedLib, ignore)
447
+ mergeOneLibrary(context, sharedLib, ignore, libraryLabel(entry, sharedLib, i))
356
448
  }
357
449
  if (ownDefaultFonts.size) yieldLibraryDefaultFonts(context.designSystem, ownDefaultFonts)
358
450
  }
package/string.js CHANGED
@@ -35,10 +35,53 @@ const getNestedValue = (obj, path) => {
35
35
  }, obj)
36
36
  }
37
37
 
38
+ /**
39
+ * Is this exact string a template the AUTHOR wrote?
40
+ *
41
+ * FW-ELEMENT-TEMPLATES-RESOLVED-IN-RUNTIME-TEXT-FUNCTION-INJECTION-1.
42
+ * A `{{ key | fn }}` template invokes a project function, so resolving one
43
+ * inside a string that CAME FROM DATA turns content into code: a customer
44
+ * typing `{{ x | nkTheme }}` into a chat box flipped the theme, and
45
+ * `nkToast('info', 'literal test {{ demoShort | polyglot }} end')` rendered
46
+ * the translation. Every function on `context.functions` was reachable from
47
+ * user text, an AI reply or an API response.
48
+ *
49
+ * The contract: a template is an author-time literal. `create()` records
50
+ * every `{{ }}` string a definition declares (`__ref.__authoredTemplates`,
51
+ * see registerEffects), and only those strings resolve. A factory may
52
+ * FORWARD an authored template — the framework `Input` atom does exactly
53
+ * that with `attr.placeholder: el => el.placeholder` — but it may never
54
+ * introduce one. The parent chain counts, so a template declared on an
55
+ * ancestor and forwarded down still resolves; every entry in every set is a
56
+ * source literal, so no state-derived string is ever in one.
57
+ *
58
+ * `allowRuntimeTemplates: true` on an element is the explicit opt-out, for
59
+ * the rare surface that really does build templates at runtime. It re-opens
60
+ * function invocation from that element's content — never set it on an
61
+ * element that renders text a user or a model produced.
62
+ *
63
+ * This reads `this` by duck-typing (`__ref`, `parent`), exactly as the state
64
+ * and polyglot lookups below already do — `utils` never imports `element`.
65
+ * A caller with no `__ref` is not an element (a bare util call, a test) and
66
+ * keeps the old unconditional behaviour.
67
+ */
68
+ const isAuthoredTemplate = (element, str) => {
69
+ if (!element || !element.__ref) return true
70
+ let el = element
71
+ while (el) {
72
+ if (el.allowRuntimeTemplates === true) return true
73
+ const authored = el.__ref && el.__ref.__authoredTemplates
74
+ if (authored && authored.has(str)) return true
75
+ el = el.parent
76
+ }
77
+ return false
78
+ }
79
+
38
80
  export function replaceLiteralsWithObjectFields (str, state, options = {}) {
39
81
  const { bracketsLength = 2 } = options
40
82
  const bracketPattern = bracketsLength === 3 ? '{{{' : '{{'
41
83
  if (!str.includes(bracketPattern)) return str
84
+ if (!isAuthoredTemplate(this, str)) return str
42
85
 
43
86
  const reg = brackRegex[bracketsLength]
44
87
  const obj = state || this.state || {}