@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.
- package/dist/cjs/keys.js +1 -1
- package/dist/cjs/methods.js +7 -7
- package/dist/cjs/object.js +19 -18
- package/dist/cjs/sharedLibraries.js +1 -1
- package/dist/cjs/string.js +3 -3
- package/dist/esm/keys.js +1 -1
- package/dist/esm/methods.js +4 -4
- package/dist/esm/object.js +19 -18
- package/dist/esm/sharedLibraries.js +1 -1
- package/dist/esm/string.js +3 -3
- package/keys.js +37 -0
- package/methods.js +24 -1
- package/object.js +378 -12
- package/package.json +1 -1
- package/sharedLibraries.js +96 -4
- package/string.js +43 -0
package/sharedLibraries.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || {}
|