Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
104f272
Update README.md
Olexandr88 Sep 3, 2025
3edd891
Bump actions/setup-node from 4 to 5 in the normal group
dependabot[bot] Sep 4, 2025
c5c47c0
[refactor] router: replace callAsync with setTimeout
kfule Sep 13, 2025
2b4b25c
[refactor] remove the render factory parameter
kfule Sep 13, 2025
af9b2b8
[refactor] domFor: split `delayedRemoval` into a separate file
kfule Sep 13, 2025
e8b37c6
[refactor] replace two duplicate `decodeURIComponentSave` functions w…
kfule Sep 13, 2025
b54bab0
[refactor] suppress the generation of intermediate variables in the b…
kfule Sep 13, 2025
992bb1f
just use setTimeout in the callAsync test helper
kfule Sep 15, 2025
9c86624
remove redundant import file paths
kfule Sep 15, 2025
35a314a
rename `decodeURIComponentSave` to `decodeURIComponentSafe`
kfule Sep 15, 2025
8a60ba2
decodeURIComponentSafe: decode only valid UTF-8 percent encoding
kfule Sep 15, 2025
c86af6c
decodeURIComponentSafe: wrap the parameter in String() for non-string…
kfule Sep 15, 2025
900dd67
add tests for decodeURIComponentSafe
kfule Sep 15, 2025
f82daf1
use RegExp() instead of regular expression literal for avoiding incor…
kfule Sep 15, 2025
e1ee9da
fix regular expression literals mangled by collision disambiguation i…
kfule Sep 17, 2025
749171e
disable Terser's `conditionals` option for performance and bundle size
kfule Sep 19, 2025
2a89704
bundler: fix regular expression literals, including their flags
kfule Sep 20, 2025
6dc1b75
Bump actions/setup-node from 5 to 6 in the normal group
dependabot[bot] Oct 14, 2025
952846b
normalizeChildren: preallocate array length and perform key-consisten…
kfule Oct 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish-prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 20
- run: npm ci
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/push-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v5
with:
ref: main
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 20
- run: npm ci
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/rollback.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:

steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 20
- run: npm ci
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 20
- run: npm ci
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

Expand Down
26 changes: 5 additions & 21 deletions api/router.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
"use strict"

var Vnode = require("../render/vnode")
var m = require("../render/hyperscript")
var hyperscript = require("../render/hyperscript")

var decodeURIComponentSafe = require("../util/decodeURIComponentSafe")
var buildPathname = require("../pathname/build")
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
}
}

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
Expand Down Expand Up @@ -63,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, decodeURIComponentSave)
.slice(route.prefix.length)
var path = decodeURIComponentSafe(prefix).slice(route.prefix.length)
var data = parsePathname(path)

Object.assign(data.params, $window.history.state)
Expand Down Expand Up @@ -126,7 +110,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)
}
}

Expand Down Expand Up @@ -189,7 +173,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
Expand Down
2 changes: 1 addition & 1 deletion api/tests/test-routerGetSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down
8 changes: 4 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"use strict"

var hyperscript = require("./hyperscript")
var request = require("./request")
var mountRedraw = require("./mount-redraw")
var domFor = require("./render/domFor")
var request = require("./request")
var router = require("./route")

var m = function m() { return hyperscript.apply(this, arguments) }
m.m = hyperscript
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
Expand All @@ -21,6 +21,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
12 changes: 3 additions & 9 deletions querystring/parse.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
"use strict"

function decodeURIComponentSave(str) {
try {
return decodeURIComponent(str)
} catch(err) {
return str
}
}
var decodeURIComponentSafe = require("../util/decodeURIComponentSafe")

module.exports = function(string) {
if (string === "" || string == null) return {}
Expand All @@ -15,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
Expand Down
3 changes: 2 additions & 1 deletion querystring/tests/test-parseQueryString.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion render.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"use strict"

module.exports = require("./render/render")(typeof window !== "undefined" ? window : null)
module.exports = require("./render/render")()
3 changes: 3 additions & 0 deletions render/delayedRemoval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"use strict"

module.exports = new WeakMap
7 changes: 2 additions & 5 deletions render/domFor.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict"

var delayedRemoval = new WeakMap
var delayedRemoval = require("./delayedRemoval")

function *domFor(vnode) {
// To avoid unintended mangling of the internal bundler,
Expand All @@ -21,7 +21,4 @@ function *domFor(vnode) {
while (domSize)
}

module.exports = {
delayedRemoval: delayedRemoval,
domFor: domFor,
}
module.exports = domFor
7 changes: 3 additions & 4 deletions render/render.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"use strict"

var Vnode = require("../render/vnode")
var df = require("../render/domFor")
var delayedRemoval = df.delayedRemoval
var domFor = df.domFor
var Vnode = require("./vnode")
var delayedRemoval = require("./delayedRemoval")
var domFor = require("./domFor")
var cachedAttrsIsStaticMap = require("./cachedAttrsIsStaticMap")

module.exports = function() {
Expand Down
8 changes: 4 additions & 4 deletions render/tests/test-domFor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 17 additions & 18 deletions render/vnode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
8 changes: 8 additions & 0 deletions scripts/_bundler-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, pre, literal) => {
const fixed = literal.replace(variables, (match) => match.replace(/\d+$/, ""))
return pre + fixed
})

//fix props
const props = new RegExp(`(\\.\\.)?((?:[^:]\\/\\/.*)?\\.\\s*)(${candidates})|([\\{,]\\s*)(${candidates})(\\s*:)`, "gm")
code = code.replace(props, (match, dotdot, dot, a, pre, b, post) => {
Expand Down
4 changes: 2 additions & 2 deletions scripts/bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
9 changes: 9 additions & 0 deletions scripts/tests/test-bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/\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 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({
"a.js": 'var b = require("./b")',
Expand Down
2 changes: 1 addition & 1 deletion test-utils/callAsync.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"use strict"

module.exports = typeof setImmediate === "function" ? setImmediate : setTimeout
module.exports = setTimeout
3 changes: 1 addition & 2 deletions util/censor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
35 changes: 35 additions & 0 deletions util/decodeURIComponentSafe.js
Original file line number Diff line number Diff line change
@@ -0,0 +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]|(?!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)
}
Loading