diff --git a/CHANGELOG.md b/CHANGELOG.md index 12fd14da..1cefcdf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.8.1 + +_29 oct 2024_ + +- Fixed issue with array based props not triggering reactivity +- Upgraded renderer to 2.5.1 + + ## v1.8.0 _22 oct 2024_ diff --git a/package-lock.json b/package-lock.json index ab8d4dcd..b99a36a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@lightningjs/blits", - "version": "1.8.0", + "version": "1.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lightningjs/blits", - "version": "1.8.0", + "version": "1.8.1", "license": "Apache-2.0", "dependencies": { "@lightningjs/msdf-generator": "^1.1.0", - "@lightningjs/renderer": "^2.4.0" + "@lightningjs/renderer": "^2.5.1" }, "bin": { "blits": "bin/index.js" @@ -787,9 +787,9 @@ } }, "node_modules/@lightningjs/renderer": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@lightningjs/renderer/-/renderer-2.4.0.tgz", - "integrity": "sha512-boh3I5imH3zXngef7bwVguGkcjs2TeDzAd3wWW+nls7LzXudG8BKPN6bGblyobP2vDVYugEhWEH3BSHH/pp5bQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@lightningjs/renderer/-/renderer-2.5.1.tgz", + "integrity": "sha512-zCnuMUBQG4BekEvXcutgPb9zZu2dxYX/hcNIE9ZGMC6Usm+p2FPSyhB3W8QqKHS64pP1hhWTcfoEO1rPKdK43Q==", "hasInstallScript": true, "engines": { "node": ">= 20.9.0", diff --git a/package.json b/package.json index 4e62605a..f90eb40a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lightningjs/blits", - "version": "1.8.0", + "version": "1.8.1", "description": "Blits: The Lightning 3 App Development Framework", "bin": "bin/index.js", "exports": { @@ -13,7 +13,7 @@ }, "scripts": { "test": "c8 npm run test:run", - "test:run": "node -r global-jsdom/register ./node_modules/.bin/tape '{,!(node_modules)/**/}*.test.js' | tap-diff", + "test:run": "node -r global-jsdom/register ./node_modules/.bin/tape '{,!(node_modules|packages)/**/}*.test.js' | tap-diff", "lint": "eslint '**/*.js'", "lint:fix": "eslint '**/*.js' --fix", "prepublishOnly": "node scripts/prepublishOnly.js", @@ -49,7 +49,7 @@ }, "dependencies": { "@lightningjs/msdf-generator": "^1.1.0", - "@lightningjs/renderer": "^2.4.0" + "@lightningjs/renderer": "^2.5.1" }, "repository": { "type": "git", diff --git a/packages/create-blits/boilerplate/ts/default/tsconfig.json b/packages/create-blits/boilerplate/ts/default/tsconfig.json index 9a361700..faf17c3e 100644 --- a/packages/create-blits/boilerplate/ts/default/tsconfig.json +++ b/packages/create-blits/boilerplate/ts/default/tsconfig.json @@ -5,6 +5,7 @@ "allowJs": true, "checkJs": true, "noEmit": true, - "strict": true + "strict": true, + "noImplicitThis": true } } diff --git a/src/helpers/deepEqualArray.js b/src/helpers/deepEqualArray.js new file mode 100644 index 00000000..68b0c065 --- /dev/null +++ b/src/helpers/deepEqualArray.js @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const deepEqualArray = (array1, array2) => { + if (array1 === array2) return true + if (array1.length !== array2.length) return false + + let l = array1.length + + while (l--) { + const value1 = array1[l] + const value2 = array2[l] + + // values are arrays, deepEqual nested arrays + if (Array.isArray(value1) && Array.isArray(value2)) { + if (deepEqualArray(value1, value2) === false) return false + } + // values are objects, deepEqual nested objects + else if ( + typeof value1 === 'object' && + value1 !== null && + typeof value2 === 'object' && + value2 !== null + ) { + if (deepEqualArray(Object.entries(value1), Object.entries(value2)) === false) return false + } + // other type, do strict equal check + else if (value1 !== value2) { + return false + } + } + + return true +} + +export default deepEqualArray diff --git a/src/helpers/deepEqualArray.test.js b/src/helpers/deepEqualArray.test.js new file mode 100644 index 00000000..060240e4 --- /dev/null +++ b/src/helpers/deepEqualArray.test.js @@ -0,0 +1,127 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'tape' +import deepEqual from './deepEqualArray.js' + +test('Type', (assert) => { + const expected = 'function' + const actual = typeof deepEqual + + assert.equal(actual, expected, 'deepEqual should be a function') + assert.end() +}) + +test('The same arrays', (assert) => { + const val1 = ['hello', 'world'] + + assert.true( + deepEqual(val1, val1), + 'deepEqual should return true if the values are the same reference' + ) + assert.end() +}) + +test('Equal simple arrays', (assert) => { + const val1 = ['hello', 'world'] + const val2 = ['hello', 'world'] + + assert.true(deepEqual(val1, val2), 'deepEqual should return true if simple arrays are equal') + assert.end() +}) + +test('Not equal simple arrays', (assert) => { + const val1 = ['hello', 'world'] + const val2 = ['world', 'hello'] + + assert.false( + deepEqual(val1, val2), + 'deepEqual should return false if simple arrays are not equal' + ) + assert.end() +}) + +test('Not Equal simple arrays (different length)', (assert) => { + const val1 = ['hello', 'world'] + const val2 = ['hello', 'world', '!'] + + assert.false(deepEqual(val1, val2), 'deepEqual should return false if simple arrays are equal') + assert.end() +}) + +test('Equal nested arrays', (assert) => { + const val1 = ['hello', ['world', '!']] + const val2 = ['hello', ['world', '!']] + + assert.true(deepEqual(val1, val2), 'deepEqual should return true if nested arrays are equal') + assert.end() +}) + +test('Not equal nested arrays', (assert) => { + const val1 = ['hello', ['world', '!']] + const val2 = ['hello', ['world', '!', '?']] + + assert.false( + deepEqual(val1, val2), + 'deepEqual should return false if nested arrays are not equal' + ) + assert.end() +}) + +test('Equal arrays with objects', (assert) => { + const val1 = [{ foo: 'bar', bar: 'foo' }, { hello: 'world' }] + const val2 = [{ foo: 'bar', bar: 'foo' }, { hello: 'world' }] + + assert.true( + deepEqual(val1, val2), + 'deepEqual should return true if arrays with objects are equal' + ) + assert.end() +}) + +test('Not equal arrays with objects (keys', (assert) => { + const val1 = [{ foo: 'bar', bar: 'foo' }, { test: 'bla' }] + const val2 = [{ foo2: 'bar', bar2: 'foo' }, { test: 'bla' }] + + assert.false( + deepEqual(val1, val2), + 'deepEqual should return false if arrays with objects are not equal' + ) + assert.end() +}) + +test('Equal arrays with mixed values', (assert) => { + const val1 = ['foo', { foo: 'bar', bar: 'foo' }, ['hello', 'world', ['this', 'is', { level: 2 }]]] + const val2 = ['foo', { foo: 'bar', bar: 'foo' }, ['hello', 'world', ['this', 'is', { level: 2 }]]] + + assert.true( + deepEqual(val1, val2), + 'deepEqual should return true if arrays with mixed values are equal' + ) + assert.end() +}) + +test('Not Equal arrays with mixed values', (assert) => { + const val1 = ['foo', { foo: 'bar', bar: 'foo' }, ['hello', 'world', ['this', 'is', { level: 2 }]]] + const val2 = ['foo', { foo: 'bar', bar: 'foo' }, ['hello', 'world', ['this', 'is', { level: 3 }]]] + + assert.false( + deepEqual(val1, val2), + 'deepEqual should return true if arrays with mixed values are not equal' + ) + assert.end() +}) diff --git a/src/lib/codegenerator/generator.js b/src/lib/codegenerator/generator.js index 193f0266..fa63a37a 100644 --- a/src/lib/codegenerator/generator.js +++ b/src/lib/codegenerator/generator.js @@ -212,12 +212,11 @@ const generateComponentCode = function ( templateObject[key], options.component )}`) - renderCode.push( - `props${counter}['${key.substring(1)}'] = ${interpolate( - templateObject[key], - options.component - )}` - ) + renderCode.push(` + props${counter}['${key.substring(1)}']= ${interpolate( + templateObject[key], + options.component + )}`) } else { renderCode.push( `props${counter}['${key}'] = ${cast(templateObject[key], key, options.component)}` diff --git a/src/lib/reactivity/reactive.js b/src/lib/reactivity/reactive.js index ec5d9f16..1be70a44 100644 --- a/src/lib/reactivity/reactive.js +++ b/src/lib/reactivity/reactive.js @@ -18,6 +18,7 @@ import { ImageTexture } from '@lightningjs/renderer' import { track, trigger, pauseTracking, resumeTracking } from './effect.js' import symbols from '../symbols.js' +import deepEqualArray from '../../helpers/deepEqualArray.js' const arrayPatchMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort'] @@ -38,7 +39,7 @@ const reactiveProxy = (original, _parent = null, _key, global) => { // if original object is already a proxy, don't create a new one but return the existing one instead const existingProxy = proxyMap.get(original) - if (existingProxy) { + if (existingProxy !== undefined) { return existingProxy } @@ -50,9 +51,9 @@ const reactiveProxy = (original, _parent = null, _key, global) => { } // handling arrays - if (Array.isArray(target)) { + if (Array.isArray(target) === true) { if (typeof target[key] === 'object' && target[key] !== null) { - if (Array.isArray(target[key])) { + if (Array.isArray(target[key]) === true) { track(target, key, global) } // create a new reactive proxy @@ -80,7 +81,7 @@ const reactiveProxy = (original, _parent = null, _key, global) => { // handling objects (but not null values, which have object type in JS) if (typeof target[key] === 'object' && target[key] !== null) { - if (Array.isArray(target[key])) { + if (Array.isArray(target[key]) === true) { track(target, key, global) } // create a new reactive proxy @@ -99,13 +100,19 @@ const reactiveProxy = (original, _parent = null, _key, global) => { const rawValue = getRaw(value) let result = true - if (oldRawValue !== rawValue) { + const isEqual = + Array.isArray(rawValue) === true + ? deepEqualArray(oldRawValue, rawValue) + : oldRawValue === rawValue + + if (isEqual === false) { + if (Array.isArray(value) === true) value = getRaw(value).slice(0) result = Reflect.set(target, key, value, receiver) } - if (result && oldRawValue !== rawValue) { + if (result === true && isEqual === false) { // if we're assigning an array key directly trigger reactivity on the parent key as well - if (Array.isArray(target) && key in target) { + if (Array.isArray(target) === true && key in target === true) { trigger(_parent, _key, true) } trigger(target, key, true) @@ -126,7 +133,7 @@ const reactiveDefineProperty = (target, global) => { if (target[key] !== null && typeof target[key] === 'object') { if (Object.getPrototypeOf(target[key]) === Object.prototype) { return reactiveDefineProperty(target[key]) - } else if (Array.isArray(target[key])) { + } else if (Array.isArray(target[key]) === true) { for (let i = 0; i < arrayPatchMethods.length - 1; i++) { target[key][arrayPatchMethods[i]] = function (v) { Array.prototype[arrayPatchMethods[i]].call(this, v)