diff --git a/lib/utils/display.js b/lib/utils/display.js index 4030c8e32b931..416824dbc230e 100644 --- a/lib/utils/display.js +++ b/lib/utils/display.js @@ -101,7 +101,25 @@ const getArrayOrObject = (items) => { return Object.assign({}, ...items.filter(o => isPlainObject(o))) } -const redactValue = (obj) => JSON.parse(redactLog(JSON.stringify(obj))) +const redactStrings = (obj) => { + if (typeof obj === 'string') { + return redactLog(obj) + } + if (Array.isArray(obj)) { + return obj.map(redactStrings) + } + if (isPlainObject(obj)) { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, redactStrings(value)]) + ) + } + return obj +} + +// Redact each string value on its own. Redacting the serialized document +// instead lets a url inside one value match across the json around it and +// corrupt the output so it no longer parses. +const redactValue = (obj) => redactStrings(JSON.parse(JSON.stringify(obj))) const getJsonBuffer = ({ [JSON_ERROR_KEY]: metaError }, buffer) => { const items = [] diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js index b33ab69a36594..01a4209c56207 100644 --- a/test/lib/utils/display.js +++ b/test/lib/utils/display.js @@ -300,6 +300,36 @@ t.test('json output redacts by default', async t => { 'inline redact: false preserves uuid values') }) +t.test('json output with a url inside a value stays valid json', async t => { + const { META } = require('proc-log') + const { output, outputs } = await mockDisplay(t) + + output.buffer({ + dependencies: { + '@scope/dep-a': { + deprecated: 'Merged into tsx: https://tsx.hirok.io', + dev: true, + _id: '@scope/dep-a@1.0.0', + }, + }, + registry: 'https://user:hunter2@registry.npmjs.org/', + versions: ['1.0.0'], + released: new Date('2024-01-01'), + }) + output.flush({ [META]: true, json: true }) + + t.equal(outputs.length, 1, 'one output') + const parsed = JSON.parse(outputs[0]) + const dep = parsed.dependencies['@scope/dep-a'] + t.equal(dep.deprecated, 'Merged into tsx: https://tsx.hirok.io', + 'a url in one value does not swallow the values after it') + t.equal(dep._id, '@scope/dep-a@1.0.0', 'later values are intact') + t.strictSame(parsed.versions, ['1.0.0'], 'arrays are walked too') + t.equal(parsed.registry, 'https://user:***@registry.npmjs.org/', + 'url passwords are still redacted') + t.equal(parsed.released, '2024-01-01T00:00:00.000Z', 'toJSON values survive the round trip') +}) + t.test('prompt functionality', async t => { t.test('regular prompt completion works', async t => { const { input } = await mockDisplay(t)