diff --git a/.changeset/modernize-utils-dead-polyfills.md b/.changeset/modernize-utils-dead-polyfills.md new file mode 100644 index 000000000..49a9fd19f --- /dev/null +++ b/.changeset/modernize-utils-dead-polyfills.md @@ -0,0 +1,33 @@ +--- +"@tko/utils": minor +"@tko/utils.parser": patch +"@tko/observable": patch +"@tko/binding.core": patch +"@tko/binding.foreach": patch +"@tko/computed": patch +"@tko/lifecycle": patch +"@tko/builder": minor +--- + +Drop dead polyfill probes from `@tko/utils` + +Removes runtime feature detection for capabilities that all supported runtimes +(modern browsers, Node, Bun, happy-dom) already expose unconditionally: + +- `functionSupportsLengthOverwrite` + `overwriteLengthPropertyIfSupported` — + `Object.defineProperty(fn, 'length', …)` has worked since IE9. Call sites + in `@tko/observable` now invoke `Object.defineProperty` directly. +- `useSymbols` + `createSymbolOrString` — `Symbol` is always defined; call + sites now use `Symbol(identifier)` directly. `createSymbolOrString` is no + longer exposed on `ko.utils` (public API removal — minor bump for + `@tko/utils` and `@tko/builder`). +- `stringTrim` + `stringStartsWith` — removed; call sites use + `String(value ?? '').trim()` / `value.startsWith(prefix)` inline. +- `toggleDomNodeCssClass` SVGAnimatedString fallback — `classList` is + available on every supported `Element` (including SVG since SVG2). +- `parseJson` no longer routes through `stringTrim`; it trims inline when the + input is a string. + +`packages/utils.parser/src/preparse.ts` also guards `str.match(bindingToken)` +against the `null` return case using `?? []` — previously relied on the match +never returning `null` for the transformed input. diff --git a/builds/knockout/spec/bindingAttributeBehaviors.js b/builds/knockout/spec/bindingAttributeBehaviors.js index d932df049..f03a06a30 100644 --- a/builds/knockout/spec/bindingAttributeBehaviors.js +++ b/builds/knockout/spec/bindingAttributeBehaviors.js @@ -260,10 +260,6 @@ describe('Binding attribute syntax', function () { ko.applyBindings({}, testNode) var allowedProperties = ['$parents', '$root', 'ko', '$rawData', '$data', '$parentContext', '$parent'] - if (ko.utils.createSymbolOrString('') === '') { - allowedProperties.push('_subscribable') - allowedProperties.push('_ancestorBindingInfo') - } ko.utils.objectForEach(ko.contextFor(testNode.childNodes[0].childNodes[0]), function (prop) { expect(allowedProperties).to.contain(prop) }) diff --git a/packages/binding.core/src/css.ts b/packages/binding.core/src/css.ts index 0d887734c..de6f93543 100644 --- a/packages/binding.core/src/css.ts +++ b/packages/binding.core/src/css.ts @@ -1,4 +1,4 @@ -import { createSymbolOrString, toggleDomNodeCssClass, objectForEach, stringTrim } from '@tko/utils' +import { toggleDomNodeCssClass, objectForEach } from '@tko/utils' import { unwrap } from '@tko/observable' @@ -12,11 +12,11 @@ export const css = { toggleDomNodeCssClass(element, className, shouldHaveClass) }) } else { - value = stringTrim(String(value || '')) // Make sure we don't try to store or set a non-string value + value = String(value ?? '').trim() // Make sure we don't try to store or set a non-string value toggleDomNodeCssClass(element, element[css.classesWrittenByBindingKey], false) element[css.classesWrittenByBindingKey] = value toggleDomNodeCssClass(element, value, true) } }, - classesWrittenByBindingKey: createSymbolOrString('__ko__cssValue') + classesWrittenByBindingKey: Symbol('__ko__cssValue') } diff --git a/packages/binding.core/src/hasfocus.ts b/packages/binding.core/src/hasfocus.ts index d0067f265..050a95540 100644 --- a/packages/binding.core/src/hasfocus.ts +++ b/packages/binding.core/src/hasfocus.ts @@ -1,11 +1,11 @@ -import { createSymbolOrString, triggerEvent, registerEventHandler } from '@tko/utils' +import { triggerEvent, registerEventHandler } from '@tko/utils' import { unwrap, dependencyDetection, isWriteableObservable } from '@tko/observable' import type { AllBindings } from '@tko/bind' -const hasfocusUpdatingProperty = createSymbolOrString('__ko_hasfocusUpdating') -const hasfocusLastValue = createSymbolOrString('__ko_hasfocusLastValue') +const hasfocusUpdatingProperty = Symbol('__ko_hasfocusUpdating') +const hasfocusLastValue = Symbol('__ko_hasfocusLastValue') export const hasfocus = { init: function (element, valueAccessor, _allBindings: AllBindings) { diff --git a/packages/binding.core/src/value.ts b/packages/binding.core/src/value.ts index 1006551b1..aad4f7968 100644 --- a/packages/binding.core/src/value.ts +++ b/packages/binding.core/src/value.ts @@ -1,4 +1,4 @@ -import { stringStartsWith, safeSetTimeout, tagNameLower, arrayForEach, selectExtensions } from '@tko/utils' +import { safeSetTimeout, tagNameLower, arrayForEach, selectExtensions } from '@tko/utils' import { unwrap, dependencyDetection } from '@tko/observable' @@ -82,7 +82,7 @@ export class value extends BindingHandler { // This is useful, for example, to catch "keydown" events after the browser has updated the control // (otherwise, selectExtensions.readValue(this) will receive the control's value *before* the key event) let handler = this.valueUpdateHandler.bind(this) - if (stringStartsWith(eventName, 'after')) { + if (eventName.startsWith('after')) { handler = () => { // The elementValueBeforeEvent variable is non-null *only* during the brief gap between // a keyX event firing and the valueUpdateHandler running, which is scheduled to happen diff --git a/packages/binding.foreach/src/foreach.ts b/packages/binding.foreach/src/foreach.ts index 4c5965ca8..72ef35ce6 100644 --- a/packages/binding.foreach/src/foreach.ts +++ b/packages/binding.foreach/src/foreach.ts @@ -5,15 +5,7 @@ // Employing sound techniques to make a faster Knockout foreach binding. // -------- -import { - arrayForEach, - cleanNode, - options, - virtualElements, - createSymbolOrString, - domData, - domNodeIsContainedBy -} from '@tko/utils' +import { arrayForEach, cleanNode, options, virtualElements, domData, domNodeIsContainedBy } from '@tko/utils' import { isObservable, unwrap, observable } from '@tko/observable' @@ -87,7 +79,7 @@ function valueToChangeAddItem(value, index): ChangeAddItem { } // store a symbol for caching the pending delete info index in the data item objects -const PENDING_DELETE_INDEX_SYM = createSymbolOrString('_ko_ffe_pending_delete_index') +const PENDING_DELETE_INDEX_SYM = Symbol('_ko_ffe_pending_delete_index') export class ForEachBinding extends AsyncBindingHandler { // NOTE: valid valueAccessors include: diff --git a/packages/builder/src/Builder.ts b/packages/builder/src/Builder.ts index a5e1da4be..3295ba213 100644 --- a/packages/builder/src/Builder.ts +++ b/packages/builder/src/Builder.ts @@ -17,7 +17,6 @@ import { cleanNode, cloneNodes, compareArrays, - createSymbolOrString, domData, extend, memoization, @@ -116,7 +115,6 @@ export type Utils = { arrayRemoveItem: typeof arrayRemoveItem cloneNodes: typeof cloneNodes compareArrays: typeof compareArrays - createSymbolOrString: typeof createSymbolOrString domData: typeof domData domNodeDisposal: typeof domNodeDisposal extend: typeof extend @@ -149,7 +147,6 @@ const utils: Utils = { arrayRemoveItem, cloneNodes, compareArrays, - createSymbolOrString, domData, domNodeDisposal, extend, diff --git a/packages/computed/src/computed.ts b/packages/computed/src/computed.ts index 81c941d78..81c66684d 100644 --- a/packages/computed/src/computed.ts +++ b/packages/computed/src/computed.ts @@ -6,7 +6,6 @@ import { addDisposeCallback, arrayForEach, - createSymbolOrString, domNodeIsAttachedToDocument, extend, options, @@ -28,7 +27,7 @@ import { import type { Observable, Subscribable } from '@tko/observable' -const computedState: symbol = createSymbolOrString('_state') +const computedState: symbol = Symbol('_state') const DISPOSED_STATE = { dependencyTracking: null, dependenciesCount: 0, diff --git a/packages/lifecycle/src/LifeCycle.ts b/packages/lifecycle/src/LifeCycle.ts index afbe73040..528d8a06f 100644 --- a/packages/lifecycle/src/LifeCycle.ts +++ b/packages/lifecycle/src/LifeCycle.ts @@ -1,10 +1,10 @@ -import { addDisposeCallback, createSymbolOrString } from '@tko/utils' +import { addDisposeCallback } from '@tko/utils' import { computed } from '@tko/computed' import type { Observable } from '@tko/observable' -const SUBSCRIPTIONS = createSymbolOrString('LifeCycle Subscriptions List') -const ANCHOR_NODE = createSymbolOrString('LifeCycle Anchor Node') +const SUBSCRIPTIONS = Symbol('LifeCycle Subscriptions List') +const ANCHOR_NODE = Symbol('LifeCycle Anchor Node') export default class LifeCycle { // NOTE: For more advanced integration as an ES6 mixin, see e.g.: diff --git a/packages/observable/src/observable.ts b/packages/observable/src/observable.ts index 4b0567dee..b31789fd0 100644 --- a/packages/observable/src/observable.ts +++ b/packages/observable/src/observable.ts @@ -2,7 +2,7 @@ // Observable values // --- // -import { options, overwriteLengthPropertyIfSupported } from '@tko/utils' +import { options } from '@tko/utils' import * as dependencyDetection from './dependencyDetection' import { deferUpdates } from './defer' @@ -103,7 +103,7 @@ export function observable(initialValue?: T): Observable { } } - overwriteLengthPropertyIfSupported(Observable as any, { value: undefined }) + Object.defineProperty(Observable, 'length', { value: undefined }) Observable[LATEST_VALUE] = initialValue diff --git a/packages/observable/src/observableArray.ts b/packages/observable/src/observableArray.ts index 5f4350aca..c911fa093 100644 --- a/packages/observable/src/observableArray.ts +++ b/packages/observable/src/observableArray.ts @@ -2,7 +2,7 @@ // Observable Arrays // === // -import { arrayIndexOf, arrayForEach, overwriteLengthPropertyIfSupported } from '@tko/utils' +import { arrayIndexOf, arrayForEach } from '@tko/utils' import type { CompareArraysOptions } from '@tko/utils' @@ -152,7 +152,7 @@ export function observableArray(initialValues?: T[]): ObservableArray trackArrayChanges(result) // ^== result.extend({ trackArrayChanges: true }) - overwriteLengthPropertyIfSupported(result, { get: () => result()?.length }) + Object.defineProperty(result, 'length', { get: () => result()?.length }) return result } diff --git a/packages/utils.parser/src/preparse.ts b/packages/utils.parser/src/preparse.ts index 545e7c36e..be9fc165e 100644 --- a/packages/utils.parser/src/preparse.ts +++ b/packages/utils.parser/src/preparse.ts @@ -1,5 +1,3 @@ -import { stringTrim } from '@tko/utils' - /* eslint no-cond-assign: 0 */ // The following regular expressions will be used to split an object-literal string into tokens @@ -42,7 +40,7 @@ const keywordRegexLookBehind = { in: 1, return: 1, typeof: 1 } */ export default function parseObjectLiteral(objectLiteralString) { // Trim leading and trailing spaces from the string - let str = stringTrim(objectLiteralString) + let str = String(objectLiteralString ?? '').trim() // Trim braces '{' surrounding the whole object literal if (str.charCodeAt(0) === 123) str = str.slice(1, -1) @@ -53,7 +51,7 @@ export default function parseObjectLiteral(objectLiteralString) { // Split into tokens const result = new Array() - let toks = str.match(bindingToken) + let toks = str.match(bindingToken) ?? [] let key let values = new Array() let depth = 0 @@ -92,7 +90,7 @@ export default function parseObjectLiteral(objectLiteralString) { if (match && !keywordRegexLookBehind[match[0]]) { // The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash) str = str.substring(str.indexOf(tok) + 1) - toks = str.match(bindingToken) + toks = str.match(bindingToken) ?? [] i = -1 // Continue with just the slash tok = '/' diff --git a/packages/utils/src/css.ts b/packages/utils/src/css.ts index eae973a6b..09006a991 100644 --- a/packages/utils/src/css.ts +++ b/packages/utils/src/css.ts @@ -2,38 +2,21 @@ // DOM - CSS // -import { arrayForEach, addOrRemoveItem } from './array' - -// For details on the pattern for changing node classes -// see: https://github.com/knockout/knockout/issues/1597 +// See: https://github.com/knockout/knockout/issues/1597 const cssClassNameRegex = /\S+/g function toggleDomNodeCssClass(node: Element, classNames: string, shouldHaveClass?: boolean): void { - let addOrRemoveFn if (!classNames) { return } - if (typeof node.classList === 'object') { - addOrRemoveFn = node.classList[shouldHaveClass ? 'add' : 'remove'] - arrayForEach(classNames.match(cssClassNameRegex)!, function (className) { - addOrRemoveFn.call(node.classList, className) - }) - } else if (typeof node.className['baseVal'] === 'string') { - // SVG tag .classNames is an SVGAnimatedString instance - toggleObjectClassPropertyString(node.className, 'baseVal', classNames, shouldHaveClass) - } else { - // node.className ought to be a string. - toggleObjectClassPropertyString(node, 'className', classNames, shouldHaveClass) + const tokens = classNames.match(cssClassNameRegex) + if (!tokens) { + return + } + const method = shouldHaveClass ? 'add' : 'remove' + for (const token of tokens) { + node.classList[method](token) } -} - -function toggleObjectClassPropertyString(obj, prop, classNames, shouldHaveClass) { - // obj/prop is either a node/'className' or a SVGAnimatedString/'baseVal'. - const currentClassNames = obj[prop].match(cssClassNameRegex) || [] - arrayForEach(classNames.match(cssClassNameRegex), function (className) { - addOrRemoveItem(currentClassNames, className, shouldHaveClass) - }) - obj[prop] = currentClassNames.join(' ') } export { toggleDomNodeCssClass } diff --git a/packages/utils/src/function.ts b/packages/utils/src/function.ts deleted file mode 100644 index ab66ea657..000000000 --- a/packages/utils/src/function.ts +++ /dev/null @@ -1,16 +0,0 @@ -function testOverwrite() { - try { - Object.defineProperty(function x() {}, 'length', {}) - return true - } catch (e) { - return false - } -} - -export const functionSupportsLengthOverwrite = testOverwrite() - -export function overwriteLengthPropertyIfSupported(fn, descriptor) { - if (functionSupportsLengthOverwrite) { - Object.defineProperty(fn, 'length', descriptor) - } -} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index d96baf035..a55923b00 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -8,9 +8,7 @@ export * from './array' export * from './async' export * from './error' export * from './object' -export * from './function' export * from './string' -export * from './symbol' export * from './css' export { default as options, defineOption, Options } from './options' diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 5a560c375..45a278bc2 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -2,27 +2,11 @@ // String (and JSON) // -export function stringTrim(string) { - return string === null || string === undefined - ? '' - : string.trim - ? string.trim() - : string.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g, '') -} - -export function stringStartsWith(string, startsWith) { - string = string || '' - if (startsWith.length > string.length) { - return false - } - return string.substring(0, startsWith.length) === startsWith -} - export function parseJson(jsonString: string): T | null { if (typeof jsonString === 'string') { - jsonString = stringTrim(jsonString) - if (jsonString) { - return JSON.parse(jsonString) as T + const trimmed = jsonString.trim() + if (trimmed) { + return JSON.parse(trimmed) as T } } return null diff --git a/packages/utils/src/symbol.ts b/packages/utils/src/symbol.ts deleted file mode 100644 index 03c126969..000000000 --- a/packages/utils/src/symbol.ts +++ /dev/null @@ -1,9 +0,0 @@ -// -// ES6 Symbols -// - -export const useSymbols = typeof Symbol === 'function' - -export function createSymbolOrString(identifier) { - return useSymbols ? Symbol(identifier) : identifier -} diff --git a/packages/utils/src/tasks.ts b/packages/utils/src/tasks.ts index ba3c6ca58..9660fcd85 100644 --- a/packages/utils/src/tasks.ts +++ b/packages/utils/src/tasks.ts @@ -15,12 +15,7 @@ const schedulerGlobal = options.global if (schedulerGlobal && typeof schedulerGlobal.queueMicrotask === 'function') { options.taskScheduler = callback => schedulerGlobal.queueMicrotask(callback) -} else if ( - schedulerGlobal && - schedulerGlobal.MutationObserver && - schedulerGlobal.document && - !(schedulerGlobal.navigator && schedulerGlobal.navigator.standalone) -) { +} else if (schedulerGlobal?.MutationObserver && schedulerGlobal.document && !schedulerGlobal.navigator?.standalone) { options.taskScheduler = (function () { let scheduledCallback: null | (() => void) = null let toggle = false