From cb81893113f9d5ad4d8ef4731838ea5fff863384 Mon Sep 17 00:00:00 2001 From: Andrey Gubanov Date: Tue, 20 Oct 2026 15:29:53 +0400 Subject: [PATCH 1/7] fix: sweep 5 es5 --- scripts/check-legacy-run.mjs | 8 + src/_dom/index.ts | 2 +- src/_helpers/arrayfrom.ts | 10 +- src/_helpers/assign.ts | 5 +- src/_helpers/forown.ts | 3 +- src/_helpers/fromentries.ts | 11 +- src/binders/html.ts | 2 +- src/binders/output.ts | 2 +- src/binders/text.ts | 2 +- src/class.ts | 16 +- .../_definehiddencontentproperty.ts | 2 +- test/legacy/scenario.js | 38 +++ test/spec/regressions/sweep5_es5_spec.js | 245 ++++++++++++++++++ 13 files changed, 327 insertions(+), 19 deletions(-) create mode 100644 test/spec/regressions/sweep5_es5_spec.js diff --git a/scripts/check-legacy-run.mjs b/scripts/check-legacy-run.mjs index c4167118..0a632e80 100644 --- a/scripts/check-legacy-run.mjs +++ b/scripts/check-legacy-run.mjs @@ -42,6 +42,14 @@ function toIE11(window) { 'getOwnPropertySymbols', 'getOwnPropertyDescriptors' ]); + // ES5's Object.keys: a primitive throws + const { keys } = window.Object; + window.Object.keys = (object) => { + if (object === null || (typeof object !== 'object' && typeof object !== 'function')) { + throw new window.TypeError('Object.keys: argument is not an Object'); + } + return keys(object); + }; drop(window.Array, ['from', 'of']); drop(window.Array.prototype, [ 'includes', diff --git a/src/_dom/index.ts b/src/_dom/index.ts index 9564293d..e0dbb0da 100644 --- a/src/_dom/index.ts +++ b/src/_dom/index.ts @@ -29,7 +29,7 @@ const wrapMap: Record = { thead: [1, '', '
'], tr: [2, '', '
'], td: [3, '', '
'], - col: [2, '', '
'], + col: [2, '', '
'], area: [1, '', ''] }; wrapMap.optgroup = wrapMap.option; diff --git a/src/_helpers/arrayfrom.ts b/src/_helpers/arrayfrom.ts index 88dbb7eb..a52c378c 100644 --- a/src/_helpers/arrayfrom.ts +++ b/src/_helpers/arrayfrom.ts @@ -1,8 +1,8 @@ -import toArray from './toarray.js'; - -// Array.from where the browser has it, so iterables work too; IE 11 takes array-likes only +// Array.from where the browser has it, so iterables work too; IE 11 takes array-likes only, holes included export default function arrayFrom(value: any, mapFn?: (value: any, index: number) => any, thisArg?: any): any[] { if (Array.from) return Array.from(value, mapFn as (value: any, index: number) => any, thisArg); - const result = toArray(value); - return mapFn ? result.map(mapFn, thisArg) : result; + const length = Math.max(0, Math.floor(value.length) || 0); + const result: any[] = []; + for (let i = 0; i < length; i++) result.push(mapFn ? mapFn.call(thisArg, value[i], i) : value[i]); + return result; } diff --git a/src/_helpers/assign.ts b/src/_helpers/assign.ts index 8cce9dd0..58f4f704 100644 --- a/src/_helpers/assign.ts +++ b/src/_helpers/assign.ts @@ -5,8 +5,9 @@ const isEnumerable = Object.prototype.propertyIsEnumerable; // Object.assign, which IE 11 lacks; symbol keys are copied where the browser has them const assign: typeof Object.assign = function assign(target: any) { for (let i = 1; i < arguments.length; i++) { - const source = arguments[i]; - if (source !== null && source !== undefined) { + if (arguments[i] !== null && arguments[i] !== undefined) { + // a primitive is read as an object: IE 11's Object.keys throws on it + const source = Object(arguments[i]); forEach(Object.keys(source), (key) => { target[key] = source[key]; }); diff --git a/src/_helpers/forown.ts b/src/_helpers/forown.ts index 3d0e9086..8ad5ba54 100644 --- a/src/_helpers/forown.ts +++ b/src/_helpers/forown.ts @@ -1,5 +1,6 @@ export default function forOwn(obj: Record, callback: (value: T, key: string) => void): void { - const keys = Object.keys(obj); + // Object(): IE 11's Object.keys throws on a primitive + const keys = Object.keys(Object(obj)); const l = keys.length; let i = 0; let key: string; diff --git a/src/_helpers/fromentries.ts b/src/_helpers/fromentries.ts index 9adc8cdf..10628ef4 100644 --- a/src/_helpers/fromentries.ts +++ b/src/_helpers/fromentries.ts @@ -1,6 +1,13 @@ -// Object.fromEntries for an array of pairs; IE 11 lacks it +// Object.fromEntries for an array of pairs; IE 11 lacks it. Defined, not assigned, so "__proto__" stays a key export default function fromEntries(entries: [string, any][]): Record { const result: Record = {}; - for (let i = 0; i < entries.length; i++) result[entries[i][0]] = entries[i][1]; + for (let i = 0; i < entries.length; i++) { + Object.defineProperty(result, entries[i][0], { + value: entries[i][1], + writable: true, + enumerable: true, + configurable: true + }); + } return result; } diff --git a/src/binders/html.ts b/src/binders/html.ts index 996f4eb3..8cde5307 100644 --- a/src/binders/html.ts +++ b/src/binders/html.ts @@ -15,7 +15,7 @@ export default function html(mappingFn?: (value: any) => any): Binder { this.innerHTML = ''; this.appendChild(val); } else { - this.innerHTML = val === null || val === undefined ? '' : `${val}`; + this.innerHTML = val === null || val === undefined ? '' : String(val); } } }; diff --git a/src/binders/output.ts b/src/binders/output.ts index a4d1cfcc..db45594d 100644 --- a/src/binders/output.ts +++ b/src/binders/output.ts @@ -9,7 +9,7 @@ export default function output(): Binder { }, setValue(this: HTMLOutputElement, value: any) { const property = 'form' in this ? 'value' : 'textContent'; - this[property] = value === null || value === undefined ? '' : `${value}`; + this[property] = value === null || value === undefined ? '' : String(value); } }; } diff --git a/src/binders/text.ts b/src/binders/text.ts index 725378c7..472f99fe 100644 --- a/src/binders/text.ts +++ b/src/binders/text.ts @@ -10,7 +10,7 @@ export default function text(mappingFn?: (value: any) => any): Binder { }, setValue(this: Node, value: any) { const val = typeof mappingFn === 'function' ? mappingFn(value) : value; - this.textContent = val === null || val === undefined ? '' : `${val}`; + this.textContent = val === null || val === undefined ? '' : String(val); } }; } diff --git a/src/class.ts b/src/class.ts index 6de68032..703dc3f5 100644 --- a/src/class.ts +++ b/src/class.ts @@ -70,14 +70,21 @@ function hasReadOnlyPrototype(fn: any): boolean { return Object.getOwnPropertyDescriptor(fn, 'prototype')?.writable === false; } +// what `new` built `this` for, read the way Babel compiles new.target: a parent called with `apply` from a subclass +// constructor gets the subclass, so a chain over an ES class works in src/ and in the ES5 build alike +function newTarget(self: any, Constructor: AnyConstructor | ((...args: any[]) => any)): AnyConstructor | undefined { + return self instanceof Constructor ? self.constructor : undefined; +} + // `constructor() {}` in an object literal is a method, which `new` refuses, and an ES class has a read-only // prototype; both get a wrapper function toConstructor(fn: (...args: any[]) => any): AnyConstructor { if (isConstructor(fn)) { if (hasReadOnlyPrototype(fn)) { - return function Constructor(...args: any[]) { - return Reflect.construct(fn, args, new.target); + const Constructor = function Constructor(this: any, ...args: any[]) { + return Reflect.construct(fn, args, newTarget(this, Constructor)); } as unknown as AnyConstructor; + return Constructor; } return fn as unknown as AnyConstructor; @@ -101,9 +108,10 @@ const Class = function Class(this: any, prototype: ClassPrototype, staticProps?: } else if (Parent) { // like an ES class without a constructor, call the parent constructor with the same arguments Constructor = function EmptyConstructor(this: any, ...args: any[]) { + const target = newTarget(this, Constructor); // a plain function parent runs on `this`, so `Child.call(this)` from a subclass constructor works too - return new.target && hasReadOnlyPrototype(Parent) && hasReflect() - ? Reflect.construct(Parent, args, new.target) + return target && hasReadOnlyPrototype(Parent) && hasReflect() + ? Reflect.construct(Parent, args, target) : Parent.apply(this, args); } as unknown as AnyConstructor; } else { diff --git a/src/parsebindings/_processattribute/_definehiddencontentproperty.ts b/src/parsebindings/_processattribute/_definehiddencontentproperty.ts index d88c871b..8e1db632 100644 --- a/src/parsebindings/_processattribute/_definehiddencontentproperty.ts +++ b/src/parsebindings/_processattribute/_definehiddencontentproperty.ts @@ -39,7 +39,7 @@ export default function defineHiddenContentProperty({ object, keys, text }: Hidd // replace things like {{x}} by actual values; a function keeps "$&" and the like in a value literal for (let i = 0; i < keys.length; i++) { - const value = values[i] === null || values[i] === undefined ? '' : `${values[i]}`; + const value = values[i] === null || values[i] === undefined ? '' : String(values[i]); result = result.replace(regs[keys[i]], () => value); } diff --git a/test/legacy/scenario.js b/test/legacy/scenario.js index a32f102e..cb6f115c 100644 --- a/test/legacy/scenario.js +++ b/test/legacy/scenario.js @@ -405,6 +405,44 @@ equal(list.fill(0, 1).join('-'), '1-0-0', 'fill is its own'); }); + test(' items, Array.from holes, a primitive in instantiate, String(value) in text', function (done) { + html('
'); + var cols = new M.Array(); + cols.itemRenderer = ''; + M.bindNode(cols, 'container', '.g'); + cols.push({}, {}); + equal($('.g').getElementsByTagName('col').length, 2, 'cols rendered'); + equal( + M.Array.from({ length: 3 }, function (_, i) { + return i * 2; + }).join(), + '0,2,4', + 'Array.from range' + ); + var Point = function (data) { + this.d = data; + }; + var app = {}; + M.instantiate(app, 'p', Point); + app.p = { a: 1 }; + app.p = 5; + equal(app.p.a, 1, 'instantiate keeps the instance'); + var price = { + valueOf: function () { + return 42; + }, + toString: function () { + return '$42.00'; + } + }; + M.bindNode(app, 'price', '.m', binders.text()); + app.price = price; + later(function () { + equal($('.m').textContent, '$42.00', 'text shows toString'); + done(); + }); + }); + // runs the cases in order; window.legacyResult holds { passed, failed: [message] } when all are done var failed = []; var passed = 0; diff --git a/test/spec/regressions/sweep5_es5_spec.js b/test/spec/regressions/sweep5_es5_spec.js new file mode 100644 index 00000000..8b7404b9 --- /dev/null +++ b/test/spec/regressions/sweep5_es5_spec.js @@ -0,0 +1,245 @@ +import Class from 'src/class'; +import MatreshkaArray from 'src/array'; +import MatreshkaObject from 'src/object'; +import arrayFrom from 'src/_helpers/arrayfrom'; +import assign from 'src/_helpers/assign'; +import forOwn from 'src/_helpers/forown'; +import fromEntries from 'src/_helpers/fromentries'; +import bindNode from 'src/bindnode'; +import instantiate from 'src/instantiate'; +import parseBindings from 'src/parsebindings'; +import { html, output, text } from 'src/binders'; +import { parseHTML } from 'src/_dom'; + +// sweep 5, the ES5 build: each case also runs against dist/ (npm run test:dist), where Babel compiled the code for IE 11 +const without = (object, names, fn) => { + const saved = names.map((name) => Object.getOwnPropertyDescriptor(object, name)); + for (const name of names) { + Object.defineProperty(object, name, { value: undefined, configurable: true, writable: true }); + } + try { + return fn(); + } finally { + names.forEach((name, i) => { + if (saved[i]) Object.defineProperty(object, name, saved[i]); + else delete object[name]; + }); + } +}; + +const withoutTemplate = (fn) => { + const { createElement } = document; + document.createElement = function (name) { + return createElement.call(this, name === 'template' ? 'x-template' : name); + }; + try { + return fn(); + } finally { + document.createElement = createElement; + } +}; + +// IE 11's Object.keys is ES5's: a primitive throws +const withES5Keys = (fn) => { + const { keys } = Object; + Object.keys = (object) => { + if (object === null || (typeof object !== 'object' && typeof object !== 'function')) { + throw new TypeError('Object.keys: argument is not an Object'); + } + return keys(object); + }; + try { + return fn(); + } finally { + Object.keys = keys; + } +}; + +const names = (nodes) => nodes.map((node) => node.nodeName); + +describe('sweep 5 ES5: parseHTML without