From 104f272c46488264036efc3e93f8a6741ec153eb Mon Sep 17 00:00:00 2001 From: Olexandr88 Date: Wed, 3 Sep 2025 14:53:37 +0300 Subject: [PATCH 01/19] Update README.md Signed-off-by: Olexandr88 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64aa03a35..03f23845f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![npm Version](https://img.shields.io/npm/v/mithril.svg)](https://www.npmjs.com/package/mithril)   [![License](https://img.shields.io/npm/l/mithril.svg)](https://github.com/MithrilJS/mithril.js/blob/main/LICENSE)   [![npm Downloads](https://img.shields.io/npm/dm/mithril.svg)](https://www.npmjs.com/package/mithril)   -[![Build Status](https://img.shields.io/github/actions/workflow/status/MithrilJS/mithril.js/.github%2Fworkflows%2Ftest.yml?branch=main&event=push)](https://www.npmjs.com/package/mithril)   +[![Build Status](https://img.shields.io/github/actions/workflow/status/MithrilJS/mithril.js/.github%2Fworkflows%2Ftest.yml?branch=main&event=push)](https://github.com/MithrilJS/mithril.js/actions)   [![Donate at OpenCollective](https://img.shields.io/opencollective/all/mithriljs.svg?colorB=brightgreen)](https://opencollective.com/mithriljs)   [![Zulip, join chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://mithril.zulipchat.com/) From 3edd89156a0220d5b4210463fee0f449a7dea9c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:12:30 +0000 Subject: [PATCH 02/19] Bump actions/setup-node from 4 to 5 in the normal group Bumps the normal group with 1 update: [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/setup-node` from 4 to 5 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: normal ... Signed-off-by: dependabot[bot] --- .github/workflows/publish-prerelease.yml | 2 +- .github/workflows/push-release.yml | 2 +- .github/workflows/rollback.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 1364fa7f2..74fd8942e 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 20 - run: npm ci diff --git a/.github/workflows/push-release.yml b/.github/workflows/push-release.yml index 4d11bae73..a3f77e739 100644 --- a/.github/workflows/push-release.yml +++ b/.github/workflows/push-release.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v5 with: ref: main - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 20 - run: npm ci diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml index 1e8f428eb..3ce71da26 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/rollback.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 20 - run: npm ci diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45958296a..0506e03fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 20 - run: npm ci From c5c47c0e30c8535e5900ce89257f7224e2047b5e Mon Sep 17 00:00:00 2001 From: kfule Date: Sat, 13 Sep 2025 19:24:17 +0900 Subject: [PATCH 03/19] [refactor] router: replace callAsync with setTimeout setImmediate() is not supported in almost all environments except IE11. Furthermore, by not using setImmediate(), the assignment code to callAsync itself, which takes non-browser environments into account, can be removed. --- api/router.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/api/router.js b/api/router.js index bc4643f76..1a4848ff9 100644 --- a/api/router.js +++ b/api/router.js @@ -17,10 +17,6 @@ function decodeURIComponentSave(component) { } module.exports = function($window, mountRedraw) { - var callAsync = $window == null - // In case Mithril.js' loaded globally without the DOM, let's not break - ? null - : typeof $window.setImmediate === "function" ? $window.setImmediate : $window.setTimeout var p = Promise.resolve() var scheduled = false @@ -126,7 +122,7 @@ module.exports = function($window, mountRedraw) { // TODO: just do `mountRedraw.redraw()` here and elide the timer // dependency. Note that this will muck with tests a *lot*, so it's // not as easy of a change as it sounds. - callAsync(resolveRoute) + setTimeout(resolveRoute) } } From 2b4b25cf880be1d1f5a9a6785caee5bf5e876cc9 Mon Sep 17 00:00:00 2001 From: kfule Date: Sat, 13 Sep 2025 19:28:21 +0900 Subject: [PATCH 04/19] [refactor] remove the render factory parameter The `window` is no longer used within m.render(). --- render.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.js b/render.js index 042d78125..ac7969787 100644 --- a/render.js +++ b/render.js @@ -1,3 +1,3 @@ "use strict" -module.exports = require("./render/render")(typeof window !== "undefined" ? window : null) +module.exports = require("./render/render")() From af9b2b8802b2d89f8e773cadcba589a1ba121d52 Mon Sep 17 00:00:00 2001 From: kfule Date: Sat, 13 Sep 2025 19:46:53 +0900 Subject: [PATCH 05/19] [refactor] domFor: split `delayedRemoval` into a separate file The bundle size can be reduced by removing the intermediate object used to export domFor and delayedRemoval. Additionally, the file paths used in domFor-related require() calls have been standardized. --- index.js | 3 +-- render/delayedRemoval.js | 3 +++ render/domFor.js | 7 ++----- render/render.js | 7 +++---- render/tests/test-domFor.js | 8 ++++---- 5 files changed, 13 insertions(+), 15 deletions(-) create mode 100644 render/delayedRemoval.js diff --git a/index.js b/index.js index b6ca3406a..c06681193 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,6 @@ var hyperscript = require("./hyperscript") var request = require("./request") var mountRedraw = require("./mount-redraw") -var domFor = require("./render/domFor") var m = function m() { return hyperscript.apply(this, arguments) } m.m = hyperscript @@ -21,6 +20,6 @@ m.parsePathname = require("./pathname/parse") m.buildPathname = require("./pathname/build") m.vnode = require("./render/vnode") m.censor = require("./util/censor") -m.domFor = domFor.domFor +m.domFor = require("./render/domFor") module.exports = m diff --git a/render/delayedRemoval.js b/render/delayedRemoval.js new file mode 100644 index 000000000..3fca3c918 --- /dev/null +++ b/render/delayedRemoval.js @@ -0,0 +1,3 @@ +"use strict" + +module.exports = new WeakMap diff --git a/render/domFor.js b/render/domFor.js index 89a5dd966..1ec558d50 100644 --- a/render/domFor.js +++ b/render/domFor.js @@ -1,6 +1,6 @@ "use strict" -var delayedRemoval = new WeakMap +var delayedRemoval = require("../render/delayedRemoval") function *domFor(vnode) { // To avoid unintended mangling of the internal bundler, @@ -21,7 +21,4 @@ function *domFor(vnode) { while (domSize) } -module.exports = { - delayedRemoval: delayedRemoval, - domFor: domFor, -} +module.exports = domFor diff --git a/render/render.js b/render/render.js index 1c97dfab1..d6f94dc6c 100644 --- a/render/render.js +++ b/render/render.js @@ -1,10 +1,9 @@ "use strict" var Vnode = require("../render/vnode") -var df = require("../render/domFor") -var delayedRemoval = df.delayedRemoval -var domFor = df.domFor -var cachedAttrsIsStaticMap = require("./cachedAttrsIsStaticMap") +var delayedRemoval = require("../render/delayedRemoval") +var domFor = require("../render/domFor") +var cachedAttrsIsStaticMap = require("../render/cachedAttrsIsStaticMap") module.exports = function() { var nameSpace = { diff --git a/render/tests/test-domFor.js b/render/tests/test-domFor.js index dae83bd13..fe25a65be 100644 --- a/render/tests/test-domFor.js +++ b/render/tests/test-domFor.js @@ -4,10 +4,10 @@ const o = require("ospec") const callAsync = require("../../test-utils/callAsync") const components = require("../../test-utils/components") const domMock = require("../../test-utils/domMock") -const vdom = require("../render") -const m = require("../hyperscript") -const fragment = require("../fragment") -const domFor = require("../../render/domFor").domFor +const vdom = require("../../render/render") +const m = require("../../render/hyperscript") +const fragment = require("../../render/fragment") +const domFor = require("../../render/domFor") o.spec("domFor(vnode)", function() { let $window, root, render From e8b37c6cc21569aa97fab980619a3917161460f4 Mon Sep 17 00:00:00 2001 From: kfule Date: Sat, 13 Sep 2025 21:57:05 +0900 Subject: [PATCH 06/19] [refactor] replace two duplicate `decodeURIComponentSave` functions with one shared version --- api/router.js | 8 +------- querystring/parse.js | 8 +------- util/decodeURIComponentSave.js | 9 +++++++++ 3 files changed, 11 insertions(+), 14 deletions(-) create mode 100644 util/decodeURIComponentSave.js diff --git a/api/router.js b/api/router.js index 1a4848ff9..6e583e66e 100644 --- a/api/router.js +++ b/api/router.js @@ -8,13 +8,7 @@ var parsePathname = require("../pathname/parse") var compileTemplate = require("../pathname/compileTemplate") var censor = require("../util/censor") -function decodeURIComponentSave(component) { - try { - return decodeURIComponent(component) - } catch(e) { - return component - } -} +var decodeURIComponentSave = require("../util/decodeURIComponentSave") module.exports = function($window, mountRedraw) { var p = Promise.resolve() diff --git a/querystring/parse.js b/querystring/parse.js index 1f2300a28..32e70707c 100644 --- a/querystring/parse.js +++ b/querystring/parse.js @@ -1,12 +1,6 @@ "use strict" -function decodeURIComponentSave(str) { - try { - return decodeURIComponent(str) - } catch(err) { - return str - } -} +var decodeURIComponentSave = require("../util/decodeURIComponentSave") module.exports = function(string) { if (string === "" || string == null) return {} diff --git a/util/decodeURIComponentSave.js b/util/decodeURIComponentSave.js new file mode 100644 index 000000000..4e4dd9d69 --- /dev/null +++ b/util/decodeURIComponentSave.js @@ -0,0 +1,9 @@ +"use strict" + +module.exports = function(str) { + try { + return decodeURIComponent(str) + } catch(err) { + return str + } +} From b54bab01223fa1c84826d3d0388f5059813bb6c4 Mon Sep 17 00:00:00 2001 From: kfule Date: Sat, 13 Sep 2025 21:57:45 +0900 Subject: [PATCH 07/19] [refactor] suppress the generation of intermediate variables in the bundle by changing the module loading order and variable name Mithril's internal bundler enables module bundling without generating intermediate variables by carefully managing module loading order and variable names. This commit prevents assigning `mountRedraw`, `decodeURIComponentSave`, and `hyperscript` to intermediate variables. It also improves readability by moving all member additions for `m` to the end of the file. --- api/router.js | 7 +++---- index.js | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/router.js b/api/router.js index 6e583e66e..497c33426 100644 --- a/api/router.js +++ b/api/router.js @@ -1,15 +1,14 @@ "use strict" var Vnode = require("../render/vnode") -var m = require("../render/hyperscript") +var hyperscript = require("../render/hyperscript") +var decodeURIComponentSave = require("../util/decodeURIComponentSave") var buildPathname = require("../pathname/build") var parsePathname = require("../pathname/parse") var compileTemplate = require("../pathname/compileTemplate") var censor = require("../util/censor") -var decodeURIComponentSave = require("../util/decodeURIComponentSave") - module.exports = function($window, mountRedraw) { var p = Promise.resolve() @@ -179,7 +178,7 @@ module.exports = function($window, mountRedraw) { // // We don't strip the other parameters because for convenience we // let them be specified in the selector as well. - var child = m( + var child = hyperscript( vnode.attrs.selector || "a", censor(vnode.attrs, ["options", "params", "selector", "onclick"]), vnode.children diff --git a/index.js b/index.js index c06681193..66aab484c 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,9 @@ "use strict" var hyperscript = require("./hyperscript") -var request = require("./request") var mountRedraw = require("./mount-redraw") +var request = require("./request") +var router = require("./route") var m = function m() { return hyperscript.apply(this, arguments) } m.m = hyperscript @@ -10,7 +11,7 @@ m.trust = hyperscript.trust m.fragment = hyperscript.fragment m.Fragment = "[" m.mount = mountRedraw.mount -m.route = require("./route") +m.route = router m.render = require("./render") m.redraw = mountRedraw.redraw m.request = request.request From 992bb1f996a3126f7e1f5cdf82d54bd4d866dae4 Mon Sep 17 00:00:00 2001 From: kfule Date: Mon, 15 Sep 2025 09:31:01 +0900 Subject: [PATCH 08/19] just use setTimeout in the callAsync test helper --- test-utils/callAsync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-utils/callAsync.js b/test-utils/callAsync.js index 426964c99..2667dbf8f 100644 --- a/test-utils/callAsync.js +++ b/test-utils/callAsync.js @@ -1,3 +1,3 @@ "use strict" -module.exports = typeof setImmediate === "function" ? setImmediate : setTimeout +module.exports = setTimeout From 9c86624c82a2a5441929dddb56f7145babef53f1 Mon Sep 17 00:00:00 2001 From: kfule Date: Mon, 15 Sep 2025 09:37:56 +0900 Subject: [PATCH 09/19] remove redundant import file paths --- render/domFor.js | 2 +- render/render.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/render/domFor.js b/render/domFor.js index 1ec558d50..86516dc31 100644 --- a/render/domFor.js +++ b/render/domFor.js @@ -1,6 +1,6 @@ "use strict" -var delayedRemoval = require("../render/delayedRemoval") +var delayedRemoval = require("./delayedRemoval") function *domFor(vnode) { // To avoid unintended mangling of the internal bundler, diff --git a/render/render.js b/render/render.js index d6f94dc6c..e85c5bf6a 100644 --- a/render/render.js +++ b/render/render.js @@ -1,9 +1,9 @@ "use strict" -var Vnode = require("../render/vnode") -var delayedRemoval = require("../render/delayedRemoval") -var domFor = require("../render/domFor") -var cachedAttrsIsStaticMap = require("../render/cachedAttrsIsStaticMap") +var Vnode = require("./vnode") +var delayedRemoval = require("./delayedRemoval") +var domFor = require("./domFor") +var cachedAttrsIsStaticMap = require("./cachedAttrsIsStaticMap") module.exports = function() { var nameSpace = { From 35a314a3bdb3ce6efd95d0afafb04a4c48860f8a Mon Sep 17 00:00:00 2001 From: kfule Date: Mon, 15 Sep 2025 10:04:12 +0900 Subject: [PATCH 10/19] rename `decodeURIComponentSave` to `decodeURIComponentSafe` --- api/router.js | 4 ++-- querystring/parse.js | 6 +++--- ...{decodeURIComponentSave.js => decodeURIComponentSafe.js} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename util/{decodeURIComponentSave.js => decodeURIComponentSafe.js} (100%) diff --git a/api/router.js b/api/router.js index 497c33426..be8894396 100644 --- a/api/router.js +++ b/api/router.js @@ -3,7 +3,7 @@ var Vnode = require("../render/vnode") var hyperscript = require("../render/hyperscript") -var decodeURIComponentSave = require("../util/decodeURIComponentSave") +var decodeURIComponentSafe = require("../util/decodeURIComponentSafe") var buildPathname = require("../pathname/build") var parsePathname = require("../pathname/parse") var compileTemplate = require("../pathname/compileTemplate") @@ -56,7 +56,7 @@ module.exports = function($window, mountRedraw) { // since the representation is consistently a relatively poorly // optimized cons string. var path = prefix.concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponentSave) + .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponentSafe) .slice(route.prefix.length) var data = parsePathname(path) diff --git a/querystring/parse.js b/querystring/parse.js index 32e70707c..853acf123 100644 --- a/querystring/parse.js +++ b/querystring/parse.js @@ -1,6 +1,6 @@ "use strict" -var decodeURIComponentSave = require("../util/decodeURIComponentSave") +var decodeURIComponentSafe = require("../util/decodeURIComponentSafe") module.exports = function(string) { if (string === "" || string == null) return {} @@ -9,8 +9,8 @@ module.exports = function(string) { var entries = string.split("&"), counters = {}, data = {} for (var i = 0; i < entries.length; i++) { var entry = entries[i].split("=") - var key = decodeURIComponentSave(entry[0]) - var value = entry.length === 2 ? decodeURIComponentSave(entry[1]) : "" + var key = decodeURIComponentSafe(entry[0]) + var value = entry.length === 2 ? decodeURIComponentSafe(entry[1]) : "" if (value === "true") value = true else if (value === "false") value = false diff --git a/util/decodeURIComponentSave.js b/util/decodeURIComponentSafe.js similarity index 100% rename from util/decodeURIComponentSave.js rename to util/decodeURIComponentSafe.js From 8a60ba2c1a00362944ccd23ff1ddf18fc0c5bb50 Mon Sep 17 00:00:00 2001 From: kfule Date: Mon, 15 Sep 2025 14:57:51 +0900 Subject: [PATCH 11/19] decodeURIComponentSafe: decode only valid UTF-8 percent encoding Some router-related tests have fixed issues where UTF-8 strings were not decoded. --- api/router.js | 7 +---- api/tests/test-routerGetSet.js | 2 +- querystring/tests/test-parseQueryString.js | 3 +- util/decodeURIComponentSafe.js | 36 +++++++++++++++++++--- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/api/router.js b/api/router.js index be8894396..66c7fc792 100644 --- a/api/router.js +++ b/api/router.js @@ -52,12 +52,7 @@ module.exports = function($window, mountRedraw) { if (prefix[0] !== "/") prefix = "/" + prefix } } - // This seemingly useless `.concat()` speeds up the tests quite a bit, - // since the representation is consistently a relatively poorly - // optimized cons string. - var path = prefix.concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponentSafe) - .slice(route.prefix.length) + var path = decodeURIComponentSafe(prefix).slice(route.prefix.length) var data = parsePathname(path) Object.assign(data.params, $window.history.state) diff --git a/api/tests/test-routerGetSet.js b/api/tests/test-routerGetSet.js index 51752c80b..9846ae052 100644 --- a/api/tests/test-routerGetSet.js +++ b/api/tests/test-routerGetSet.js @@ -220,7 +220,7 @@ o.spec("route.get/route.set", function() { route.set("/other/:a/:b", {a: "x", b: "y/z", c: "d", e: "f"}) setTimeout(function() { // Yep, before even the throttle mechanism takes hold. - o(route.get()).equals("/other/x/y%2Fz?c=d&e=f") + o(route.get()).equals("/other/x/y/z?c=d&e=f") throttleMock.fire() done() }) diff --git a/querystring/tests/test-parseQueryString.js b/querystring/tests/test-parseQueryString.js index 8d497cdb6..0e7fd4faf 100644 --- a/querystring/tests/test-parseQueryString.js +++ b/querystring/tests/test-parseQueryString.js @@ -22,7 +22,8 @@ o.spec("parseQueryString", function() { }) o("handles wrongly escaped values", function() { var data = parseQueryString("?test=%c5%a1%e8ZM%80%82H") - o(data).deepEquals({test: "%c5%a1%e8ZM%80%82H"}) + // decodes "%c5%a1" only + o(data).deepEquals({test: "ลก%e8ZM%80%82H"}) }) o("handles escaped slashes followed by a number", function () { var data = parseQueryString("?hello=%2Fen%2F1") diff --git a/util/decodeURIComponentSafe.js b/util/decodeURIComponentSafe.js index 4e4dd9d69..fdac1d77b 100644 --- a/util/decodeURIComponentSafe.js +++ b/util/decodeURIComponentSafe.js @@ -1,9 +1,35 @@ "use strict" +/* +Percent encodings encode UTF-8 bytes, so this regexp needs to match that. +Here's how UTF-8 encodes stuff: +- `00-7F`: 1-byte, for U+0000-U+007F +- `C2-DF 80-BF`: 2-byte, for U+0080-U+07FF +- `E0-EF 80-BF 80-BF`: 3-byte, encodes U+0800-U+FFFF +- `F0-F4 80-BF 80-BF 80-BF`: 4-byte, encodes U+10000-U+10FFFF +In this, there's a number of invalid byte sequences: +- `80-BF`: Continuation byte, invalid as start +- `C0-C1 80-BF`: Overlong encoding for U+0000-U+007F +- `E0 80-9F 80-BF`: Overlong encoding for U+0080-U+07FF +- `ED A0-BF 80-BF`: Encoding for UTF-16 surrogate U+D800-U+DFFF +- `F0 80-8F 80-BF 80-BF`: Overlong encoding for U+0800-U+FFFF +- `F4 90-BF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. +- `F5-FF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. +So in reality, only the following sequences can encode are valid characters: +- 00-7F +- C2-DF 80-BF +- E0 A0-BF 80-BF +- E1-EC 80-BF 80-BF +- ED 80-9F 80-BF +- EE-EF 80-BF 80-BF +- F0 90-BF 80-BF 80-BF +- F1-F3 80-BF 80-BF 80-BF +- F4 80-8F 80-BF 80-BF + +The regexp just tries to match this as compactly as possible. +*/ +var validUtf8Encodings = /%(?:[0-7]|(?!c0|c1|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[\da-f]%[89ab])[\da-f]%[89ab])[\da-f]/gi + module.exports = function(str) { - try { - return decodeURIComponent(str) - } catch(err) { - return str - } + return str.replace(validUtf8Encodings, decodeURIComponent) } From c86af6c32e5e581d2d5e4161bf4244cd1d728bc0 Mon Sep 17 00:00:00 2001 From: kfule Date: Mon, 15 Sep 2025 21:28:04 +0900 Subject: [PATCH 12/19] decodeURIComponentSafe: wrap the parameter in String() for non-string types This will make the results more consistent with decodeURIComponent. --- util/decodeURIComponentSafe.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/decodeURIComponentSafe.js b/util/decodeURIComponentSafe.js index fdac1d77b..6bbaa01ba 100644 --- a/util/decodeURIComponentSafe.js +++ b/util/decodeURIComponentSafe.js @@ -31,5 +31,5 @@ The regexp just tries to match this as compactly as possible. var validUtf8Encodings = /%(?:[0-7]|(?!c0|c1|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[\da-f]%[89ab])[\da-f]%[89ab])[\da-f]/gi module.exports = function(str) { - return str.replace(validUtf8Encodings, decodeURIComponent) + return String(str).replace(validUtf8Encodings, decodeURIComponent) } From 900dd67ef2e098a8efd3e63d8a86a5b816ee2399 Mon Sep 17 00:00:00 2001 From: kfule Date: Mon, 15 Sep 2025 21:28:48 +0900 Subject: [PATCH 13/19] add tests for decodeURIComponentSafe --- util/tests/test-decodeURIComponentSafe.js | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 util/tests/test-decodeURIComponentSafe.js diff --git a/util/tests/test-decodeURIComponentSafe.js b/util/tests/test-decodeURIComponentSafe.js new file mode 100644 index 000000000..c9749e359 --- /dev/null +++ b/util/tests/test-decodeURIComponentSafe.js @@ -0,0 +1,119 @@ +"use strict" + +var o = require("ospec") +var decodeURIComponentSafe = require("../../util/decodeURIComponentSafe") + +o.spec("decodeURIComponentSafe", function() { + o("non-string type (compared to decodeURIComponent)", function() { + o(decodeURIComponentSafe()).equals(decodeURIComponent()) + o(decodeURIComponentSafe(null)).equals(decodeURIComponent(null)) + o(decodeURIComponentSafe(0)).equals(decodeURIComponent(0)) + o(decodeURIComponentSafe(true)).equals(decodeURIComponent(true)) + o(decodeURIComponentSafe(false)).equals(decodeURIComponent(false)) + o(decodeURIComponentSafe({})).equals(decodeURIComponent({})) + o(decodeURIComponentSafe([])).equals(decodeURIComponent([])) + o(decodeURIComponentSafe(function(){})).equals(decodeURIComponent(function(){})) + }) + + o("non-percent-encoded string", function() { + o(decodeURIComponentSafe("")).equals("") + o(decodeURIComponentSafe("1")).equals("1") + o(decodeURIComponentSafe("abc")).equals("abc") + o(decodeURIComponentSafe("๐Ÿ˜ƒ")).equals("๐Ÿ˜ƒ") + }) + + o("percent-encoded ASCII", function() { + for (var i = 0; i < 128; i++) { + var char = String.fromCharCode(i) + var uenc = "%" + Number(i).toString(16).padStart(2, "0").toUpperCase() + var lenc = "%" + Number(i).toString(16).padStart(2, "0").toLowerCase() + var uout = decodeURIComponentSafe(uenc) + var lout = decodeURIComponentSafe(lenc) + o(char).equals(uout) + o(char).equals(lout) + } + }) + + o("all code points (without surrogates)", function() { + var ranges = [ + [0x0000, 0xD7FF], + /* [0xD800, 0xDFFF], */ + [0xE000, 0x10FFFF] + ] + for (var [lo, hi] of ranges) { + for (var cp = lo; cp <= hi; cp++) { + var char = String.fromCodePoint(cp) + // including ASCII characters not encoded by encodeURIComponent + var enc = encodeURIComponent(char) + var out = decodeURIComponentSafe(enc) + o(char).equals(out) + } + } + }) + + o("invalid byte sequences", function() { + // `80-BF`: Continuation byte, invalid as start + o(decodeURIComponentSafe("%7F")).notEquals("%7F") + o(decodeURIComponentSafe("%80")).equals("%80") + o(decodeURIComponentSafe("%BF")).equals("%BF") + + // `C0-C1 80-BF`: Overlong encoding for U+0000-U+007F + o(decodeURIComponentSafe("%C0%80")).equals("%C0%80") // U+0000 + o(decodeURIComponentSafe("%C1%BF")).equals("%C1%BF") // U+007F + o(decodeURIComponentSafe("%C2%80")).notEquals("%C2%80") // U+0080 + + // `E0 80-9F 80-BF`: Overlong encoding for U+0080-U+07FF + o(decodeURIComponentSafe("%DF%BF")).notEquals("%DF%BF") // U+07FF + o(decodeURIComponentSafe("%E0%80%80")).equals("%E0%80%80") // U+0000 + o(decodeURIComponentSafe("%E0%9F%BF")).equals("%E0%9F%BF") // U+07FF + o(decodeURIComponentSafe("%E0%A0%80")).notEquals("%E0%A0%80") // U+0800 + + // `ED A0-BF 80-BF`: Encoding for UTF-16 surrogate U+D800-U+DFFF + o(decodeURIComponentSafe("%ED%9F%BF")).notEquals("%ED%9F%BF") // U+D7FF + o(decodeURIComponentSafe("%ED%A0%80")).equals("%ED%A0%80") // U+D800 + o(decodeURIComponentSafe("%ED%AF%BF")).equals("%ED%AF%BF") // U+DBFF + o(decodeURIComponentSafe("%ED%B0%80")).equals("%ED%B0%80") // U+DC00 + o(decodeURIComponentSafe("%ED%BF%BF")).equals("%ED%BF%BF") // U+DFFF + o(decodeURIComponentSafe("%EE%80%80")).notEquals("%EE%80%80") // U+E000 + + // `F0 80-8F 80-BF 80-BF`: Overlong encoding for U+0800-U+FFFF + o(decodeURIComponentSafe("%EF%BF%BF")).notEquals("%EF%BF%BF") // U+FFFF + o(decodeURIComponentSafe("%F0%80%80%80")).equals("%F0%80%80%80") // U+0000 + o(decodeURIComponentSafe("%E0%80%9F%BF")).equals("%E0%80%9F%BF") // U+07FF + o(decodeURIComponentSafe("%E0%80%A0%80")).equals("%E0%80%A0%80") // U+0800 + o(decodeURIComponentSafe("%F0%8F%BF%BF")).equals("%F0%8F%BF%BF") // U+FFFF + o(decodeURIComponentSafe("%F0%90%80%80")).notEquals("%F0%90%80%80") // U+10000 + + // `F4 90-BF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. + o(decodeURIComponentSafe("%F4%8F%BF%BF")).notEquals("%F4%8F%BF%BF") // U+10FFFF + o(decodeURIComponentSafe("%F4%90%80%80")).equals("%F4%90%80%80") // U+110000 + o(decodeURIComponentSafe("%F4%BF%BF%BF")).equals("%F4%BF%BF%BF") // U+13FFFF + + // `F5-FF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. + o(decodeURIComponentSafe("%F5")).equals("%F5") + o(decodeURIComponentSafe("%FF")).equals("%FF") + o(decodeURIComponentSafe("%F5%80%80%80")).equals("%F5%80%80%80") // U+140000 + o(decodeURIComponentSafe("%FF%8F%BF%BF")).equals("%FF%8F%BF%BF") + }) + + o("malformed URI sequence", function() { + // "%" only + o(() => decodeURIComponent("%")).throws(URIError) + o(decodeURIComponentSafe("%")).equals("%") + // "%" with one digit + o(() => decodeURIComponent("%1")).throws(URIError) + o(decodeURIComponentSafe("%1")).equals("%1") + // "%" with non-hexadecimal + o(() => decodeURIComponent("%G0")).throws(URIError) + o(decodeURIComponentSafe("%G0")).equals("%G0") + // "%" in string + o(() => decodeURIComponent("x%y")).throws(URIError) + o(decodeURIComponentSafe("x%y")).equals("x%y") + // Overlong encoding + o(() => decodeURIComponent("%E0%80%AF")).throws(URIError) + o(decodeURIComponentSafe("%E0%80%AF")).equals("%E0%80%AF") + // surrogate + o(() => decodeURIComponent("%ED%A0%80")).throws(URIError) + o(decodeURIComponentSafe("%ED%A0%80")).equals("%ED%A0%80") + }) +}) From f82daf1912e86c4b4e2d0d6eef5150d76faf5d0c Mon Sep 17 00:00:00 2001 From: kfule Date: Tue, 16 Sep 2025 00:34:20 +0900 Subject: [PATCH 14/19] use RegExp() instead of regular expression literal for avoiding incorrect mangling by internal bundler Because "c" was being improperly mangled and the "c0" suffix was being improperly removed, it was changed to "c[01]" rather than just dropping the literal use. Also, since the backslash in "\d" is escaped ("\\d"), I simply changed it to "0-9" of the same string length. --- util/decodeURIComponentSafe.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/util/decodeURIComponentSafe.js b/util/decodeURIComponentSafe.js index 6bbaa01ba..b1526519f 100644 --- a/util/decodeURIComponentSafe.js +++ b/util/decodeURIComponentSafe.js @@ -28,7 +28,8 @@ So in reality, only the following sequences can encode are valid characters: The regexp just tries to match this as compactly as possible. */ -var validUtf8Encodings = /%(?:[0-7]|(?!c0|c1|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[\da-f]%[89ab])[\da-f]%[89ab])[\da-f]/gi +// Words in RegExp literals are sometimes mangled incorrectly by the internal bundler, so use RegExp(). +var validUtf8Encodings = new RegExp("%(?:[0-7]|(?!c[01]|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[0-9a-f]%[89ab])[0-9a-f]%[89ab])[0-9a-f]", "gi") module.exports = function(str) { return String(str).replace(validUtf8Encodings, decodeURIComponent) From e1ee9daa217ab413b3f3edceacf2c27008edab44 Mon Sep 17 00:00:00 2001 From: kfule Date: Wed, 17 Sep 2025 19:55:39 +0900 Subject: [PATCH 15/19] fix regular expression literals mangled by collision disambiguation in the internal bundler This allows regular expression literals to be used even where RegExp() was previously used as a workaround. --- scripts/_bundler-impl.js | 8 ++++++++ scripts/tests/test-bundler.js | 9 +++++++++ util/censor.js | 3 +-- util/decodeURIComponentSafe.js | 3 +-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/scripts/_bundler-impl.js b/scripts/_bundler-impl.js index 5f289aa92..45096b97f 100644 --- a/scripts/_bundler-impl.js +++ b/scripts/_bundler-impl.js @@ -157,6 +157,14 @@ module.exports = async (input) => { return open + fixed + close }) + // fix regexp literals + // Note: This regexp, while it doesn't technically capture all cases a regexp could appear, should hopefully work for now. + const regexpLiteral = /([=({[](?:[\s\u2028\u2029]|\/\/.*?[\r\n\u2028\u2029]|\/\*[\s\S]*?\*\/)*\/)((?:[^\\\/[\r\n\u2028\u2029]|\\[^\r\n\u2028\u2029]|\[(?:[^\]\\\r\n\u2028\u2029]|\\[^\r\n\u2028\u2029])*\])+)(\/[$\p{ID_Continue}]*)/ug + code = code.replace(regexpLiteral, (match, open, data, close) => { + const fixed = data.replace(variables, (match) => match.replace(/\d+$/, "")) + return open + fixed + close + }) + //fix props const props = new RegExp(`(\\.\\.)?((?:[^:]\\/\\/.*)?\\.\\s*)(${candidates})|([\\{,]\\s*)(${candidates})(\\s*:)`, "gm") code = code.replace(props, (match, dotdot, dot, a, pre, b, post) => { diff --git a/scripts/tests/test-bundler.js b/scripts/tests/test-bundler.js index 0c9949010..ddb0aa651 100644 --- a/scripts/tests/test-bundler.js +++ b/scripts/tests/test-bundler.js @@ -285,6 +285,15 @@ o.spec("bundler", async () => { o(await bundle(p("a.js"))).equals(';(function() {\nvar b0 = "b b b \\\" b"\nvar b = function() {return b0}\n}());') }) + o("does not mess up regexp literals", async () => { + await setup({ + "a.js": 'var b = require("./b")\nvar c = require("./c")', + "b.js": "var b = /b/\nmodule.exports = function() {return b}", + "c.js": "var b =\n\t/ b \\/ \\/ [a-b]/g\nvar d = b/b\nmodule.exports = function() {return b}", + }) + + o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = /b/\nvar b = function() {return b0}\nvar b1 =\n\t/ b \\/ \\/ [a-b]/g\nvar d = b1/b1\nvar c = function() {return b1}\n}());") + }) o("does not mess up properties", async () => { await setup({ "a.js": 'var b = require("./b")', diff --git a/util/censor.js b/util/censor.js index f21ce0d0f..2fa2958c9 100644 --- a/util/censor.js +++ b/util/censor.js @@ -24,8 +24,7 @@ // ``` var hasOwn = require("./hasOwn") -// Words in RegExp literals are sometimes mangled incorrectly by the internal bundler, so use RegExp(). -var magic = new RegExp("^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$") +var magic = /^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$/ module.exports = function(attrs, extras) { var result = {} diff --git a/util/decodeURIComponentSafe.js b/util/decodeURIComponentSafe.js index b1526519f..b0698a87b 100644 --- a/util/decodeURIComponentSafe.js +++ b/util/decodeURIComponentSafe.js @@ -28,8 +28,7 @@ So in reality, only the following sequences can encode are valid characters: The regexp just tries to match this as compactly as possible. */ -// Words in RegExp literals are sometimes mangled incorrectly by the internal bundler, so use RegExp(). -var validUtf8Encodings = new RegExp("%(?:[0-7]|(?!c[01]|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[0-9a-f]%[89ab])[0-9a-f]%[89ab])[0-9a-f]", "gi") +var validUtf8Encodings = /%(?:[0-7]|(?!c[01]|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[\da-f]%[89ab])[\da-f]%[89ab])[\da-f]/gi module.exports = function(str) { return String(str).replace(validUtf8Encodings, decodeURIComponent) From 749171e6f8c2f1b6d90814518a0aec834b3460bf Mon Sep 17 00:00:00 2001 From: kfule Date: Fri, 19 Sep 2025 21:59:30 +0900 Subject: [PATCH 16/19] disable Terser's `conditionals` option for performance and bundle size Like the issue addressed in #3000, it seemed that the performance of mithril.min.js had degraded again. I tried some compression options for terser and found that disabling the `conditionals` option improved performance and also reduced the bundle size. --- scripts/bundler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/bundler.js b/scripts/bundler.js index 819fc0c98..19417b89f 100644 --- a/scripts/bundler.js +++ b/scripts/bundler.js @@ -42,8 +42,8 @@ async function build() { return } console.log("minifying...") - // Terser's "reduce_funcs" option seems to degrade performance. So, disable it. - const minified = await Terser.minify(original, {compress: {reduce_funcs: false}, mangle: true}) + // Terser's "conditionals" and "reduce_funcs" options seem to degrade performance. So, disable them. + const minified = await Terser.minify(original, {compress: {conditionals: false, reduce_funcs: false}, mangle: true}) if (minified.error) throw new Error(minified.error) await writeFile(params.output, minified.code, "utf-8") const originalSize = Buffer.byteLength(original, "utf-8") From 2a897041b8c9f7c8c81ef091af0012510e92eec8 Mon Sep 17 00:00:00 2001 From: kfule Date: Sat, 20 Sep 2025 22:58:39 +0900 Subject: [PATCH 17/19] bundler: fix regular expression literals, including their flags It seemed that, for example, `var i` might cause a suffix to be added to the "i" flag within a regular expression literal. So the entire regular expression literal, including flags, is now corrected within the bundler. --- scripts/_bundler-impl.js | 8 ++++---- scripts/tests/test-bundler.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/_bundler-impl.js b/scripts/_bundler-impl.js index 45096b97f..1536fbc62 100644 --- a/scripts/_bundler-impl.js +++ b/scripts/_bundler-impl.js @@ -159,10 +159,10 @@ module.exports = async (input) => { // fix regexp literals // Note: This regexp, while it doesn't technically capture all cases a regexp could appear, should hopefully work for now. - const regexpLiteral = /([=({[](?:[\s\u2028\u2029]|\/\/.*?[\r\n\u2028\u2029]|\/\*[\s\S]*?\*\/)*\/)((?:[^\\\/[\r\n\u2028\u2029]|\\[^\r\n\u2028\u2029]|\[(?:[^\]\\\r\n\u2028\u2029]|\\[^\r\n\u2028\u2029])*\])+)(\/[$\p{ID_Continue}]*)/ug - code = code.replace(regexpLiteral, (match, open, data, close) => { - const fixed = data.replace(variables, (match) => match.replace(/\d+$/, "")) - return open + fixed + close + const regexpLiteral = /([=({[](?:[\s\u2028\u2029]|\/\/.*?[\r\n\u2028\u2029]|\/\*[\s\S]*?\*\/)*)(\/(?:[^\\\/[\r\n\u2028\u2029]|\\[^\r\n\u2028\u2029]|\[(?:[^\]\\\r\n\u2028\u2029]|\\[^\r\n\u2028\u2029])*\])+\/[$\p{ID_Continue}]*)/ug + code = code.replace(regexpLiteral, (match, pre, literal) => { + const fixed = literal.replace(variables, (match) => match.replace(/\d+$/, "")) + return pre + fixed }) //fix props diff --git a/scripts/tests/test-bundler.js b/scripts/tests/test-bundler.js index ddb0aa651..5ab88df01 100644 --- a/scripts/tests/test-bundler.js +++ b/scripts/tests/test-bundler.js @@ -288,11 +288,11 @@ o.spec("bundler", async () => { o("does not mess up regexp literals", async () => { await setup({ "a.js": 'var b = require("./b")\nvar c = require("./c")', - "b.js": "var b = /b/\nmodule.exports = function() {return b}", + "b.js": "var b = /b/\nvar g = 0\nmodule.exports = function() {return b}", "c.js": "var b =\n\t/ b \\/ \\/ [a-b]/g\nvar d = b/b\nmodule.exports = function() {return b}", }) - o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = /b/\nvar b = function() {return b0}\nvar b1 =\n\t/ b \\/ \\/ [a-b]/g\nvar d = b1/b1\nvar c = function() {return b1}\n}());") + o(await bundle(p("a.js"))).equals(";(function() {\nvar b0 = /b/\nvar g = 0\nvar b = function() {return b0}\nvar b1 =\n\t/ b \\/ \\/ [a-b]/g\nvar d = b1/b1\nvar c = function() {return b1}\n}());") }) o("does not mess up properties", async () => { await setup({ From 6dc1b75429e30b71d25db4127d19b8c8d0a5d4e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 19:13:15 +0000 Subject: [PATCH 18/19] Bump actions/setup-node from 5 to 6 in the normal group Bumps the normal group with 1 update: [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/setup-node` from 5 to 6 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: normal ... Signed-off-by: dependabot[bot] --- .github/workflows/publish-prerelease.yml | 2 +- .github/workflows/push-release.yml | 2 +- .github/workflows/rollback.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 74fd8942e..0fe09a26e 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: 20 - run: npm ci diff --git a/.github/workflows/push-release.yml b/.github/workflows/push-release.yml index a3f77e739..f78ce62fb 100644 --- a/.github/workflows/push-release.yml +++ b/.github/workflows/push-release.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v5 with: ref: main - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: 20 - run: npm ci diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml index 3ce71da26..59e028acb 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/rollback.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: 20 - run: npm ci diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0506e03fb..1162f1e83 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: 20 - run: npm ci From 952846b057108bc09abb91867d76804402978550 Mon Sep 17 00:00:00 2001 From: kfule Date: Sun, 12 Oct 2025 21:41:45 +0900 Subject: [PATCH 19/19] normalizeChildren: preallocate array length and perform key-consistency checks after normalization This change preallocates the array to the input length and collapses multiple loops into a single pass. Assigning immediately after preallocation improves performance on V8 (generally neutral elsewhere). Key checks are now performed on normalized vnodes, making the consistency validation more accurate and clarifying the correspondence between error messages and code. Perf-sensitive comments have been clarified to reflect the original intent of commit 6c562d2. Behavior is unchanged, except that the timing/order of related errors may differ slightly. All existing tests pass. Additionally, bundle size is slightly reduced. --- render/vnode.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/render/vnode.js b/render/vnode.js index 821b21bb2..3616c0fec 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -10,24 +10,23 @@ Vnode.normalize = function(node) { return Vnode("#", undefined, undefined, String(node), undefined, undefined) } Vnode.normalizeChildren = function(input) { - var children = [] - if (input.length) { - var isKeyed = input[0] != null && input[0].key != null - // Note: this is a *very* perf-sensitive check. - // Fun fact: merging the loop like this is somehow faster than splitting - // it, noticeably so. - for (var i = 1; i < input.length; i++) { - if ((input[i] != null && input[i].key != null) !== isKeyed) { - throw new TypeError( - isKeyed && (input[i] == null || typeof input[i] === "boolean") - ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole." - : "In fragments, vnodes must either all have keys or none have keys." - ) - } - } - for (var i = 0; i < input.length; i++) { - children[i] = Vnode.normalize(input[i]) - } + // Preallocate the array length (initially holey) and fill every index immediately in order. + // Benchmarking shows better performance on V8. + var children = new Array(input.length) + // Count the number of keyed normalized vnodes for consistency check. + // Note: this is a perf-sensitive check. + // Fun fact: merging the loop like this is somehow faster than splitting + // the check within updateNodes(), noticeably so. + var numKeyed = 0 + for (var i = 0; i < input.length; i++) { + children[i] = Vnode.normalize(input[i]) + if (children[i] !== null && children[i].key != null) numKeyed++ + } + if (numKeyed !== 0 && numKeyed !== input.length) { + throw new TypeError(children.includes(null) + ? "In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole." + : "In fragments, vnodes must either all have keys or none have keys." + ) } return children }