From a02794904ab97096a76100d07dc28c6d787dbbcc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:54:55 +0000 Subject: [PATCH] dev server console: print each key of an array that is not an array index The browser console formatter listed the named keys of an array with a test on Number(key). A key such as "1.5", "01" or "" passed that test when the array was long enough, so the formatter printed it neither as an element nor as a named key. Test each own key against the canonical form of an array index. Write the label of a named key through formatPropertyKey, as formatPlainObject does, so that a restored key such as "" prints as a quoted string. --- src/runtime/bake/client/inspect.ts | 17 ++++---- test/bake/dev/esm.test.ts | 66 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/runtime/bake/client/inspect.ts b/src/runtime/bake/client/inspect.ts index 522725bb6da8..388036dc5aef 100644 --- a/src/runtime/bake/client/inspect.ts +++ b/src/runtime/bake/client/inspect.ts @@ -247,6 +247,9 @@ function formatObject(ctx: InspectContext, value: any, recurseTimes: number) { return output; } +// Hoisted so that the test of each own key in formatArray allocates nothing. +const arrayIndexKey = /^(?:0|[1-9]\d*)$/; + /** * Format an array * @param {Object} ctx - Context object @@ -275,14 +278,12 @@ function formatArray(ctx: InspectContext, value: any[], recurseTimes: number) { } // Add array properties that aren't indices - const keys = Object.keys(value).filter(key => { - return !(Number(key) >= 0 && Number(key) < value.length && Number(key) === +key); - }); - - if (keys.length > 0) { - for (const key of keys) { - output.push(`${key}: ${formatValue(ctx, value[key], recurseTimes)}`); - } + const length = value.length; + const keys = Object.keys(value); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (arrayIndexKey.test(key) && +key < length) continue; + output.push(`${formatPropertyKey(key)}: ${formatValue(ctx, value[key], recurseTimes)}`); } return `[ ${output.join(", ")} ]`; diff --git a/test/bake/dev/esm.test.ts b/test/bake/dev/esm.test.ts index 82f69cf6f377..82090a5aaeb0 100644 --- a/test/bake/dev/esm.test.ts +++ b/test/bake/dev/esm.test.ts @@ -466,6 +466,72 @@ devTest("browser field is used", { }, }); +devTest("browser console forwarding prints a key of an array that is not an array index", { + files: { + "index.html": emptyHtmlFile({ + scripts: ["index.ts"], + }), + "index.ts": ` + const mid = [1, 2, 3]; + mid[mid.length / 2] = "mid"; + const numeric = { "": 1, "01": 2, "1.5": 3 }; + console.log("drop-probe", mid, Object.assign([5, 6], numeric), numeric); + console.log("length-probe", Object.assign([5], { "1.5": 1, "01": 2 })); + console.log("range-probe", Object.assign([5], { "4294967295": "far", "-1": -1 })); + const long = new Array(150).fill(0); + long["120.5"] = "a"; + long["099"] = "b"; + console.log("long-probe", long); + console.log("nested-probe", { list: Object.assign([5, 6], { "1.5": 1 }) }); + console.log("newline-probe", Object.assign([5], { "\\n": 1 })); + console.log("hole-probe", Object.assign([1, , , 4], { "01": 1, "1.5": 2 })); + console.log("label-probe", Object.assign([5], { "a b": 1, ok: 2 }), "xabcx".match(/abc/)); + `, + "bun.app.ts": ` + import html from "./index.html"; + export default { + static: { + "/": html, + }, + development: { + console: true, + }, + fetch(req) { + return new Response("Not Found", { status: 404 }); + }, + }; + `, + }, + htmlFiles: [], + async test(dev) { + await using c = await dev.client("/"); + await c.expectMessage( + "drop-probe", + "length-probe", + "range-probe", + "long-probe", + "nested-probe", + "newline-probe", + "hole-probe", + "label-probe", + ); + await dev.output.waitForLine(/\[browser\].*label-probe/); + const forwarded = dev.output.lines.filter(line => line.includes("[browser]")).map(line => Bun.stripANSI(line)); + expect(forwarded).toEqual([ + "[browser] drop-probe [ 1, 2, 3, '1.5': 'mid' ] [ 5, 6, '': 1, '01': 2, '1.5': 3 ] { '': 1, '01': 2, '1.5': 3 }", + "[browser] length-probe [ 5, '1.5': 1, '01': 2 ]", + "[browser] range-probe [ 5, '4294967295': 'far', '-1': -1 ]", + // The first 100 elements, then the rest as a count, then the named keys. + `[browser] long-probe [ ${Buffer.alloc(300, "0, ").toString()}... 50 more items, '120.5': 'a', '099': 'b' ]`, + "[browser] nested-probe { list: [ 5, 6, '1.5': 1 ] }", + "[browser] newline-probe [ 5, '\\n': 1 ]", + // Two holes and two named keys: the array has as many own keys as elements. + "[browser] hole-probe [ 1, empty, empty, 4, '01': 1, '1.5': 2 ]", + "[browser] label-probe [ 5, 'a b': 1, ok: 2 ] [ 'abc', index: 1, input: 'xabcx', groups: undefined ]", + ]); + }, +}); + devTest("browser console forwarding strips terminal control bytes", { files: { "index.html": emptyHtmlFile({