Пусть второй параметр будет массивом индексов, по умолчанию - пустой (соответственно, когда вызываете функцию сами, его передавать не надо). В начале обработки элемента данных кладёте его индекс в массив, после обработки убираете. Ну а текст - собираете из массива какой вам нужен:
const createTreeElement = (data, index = []) =>
Array.isArray(data) && data.length
? data.reduce((ul, n, i) => (
index.push(i),
ul.append(document.createElement('li')),
ul.lastChild.append(index.join('_'), createTreeElement(n.children, index)),
index.pop(),
ul
), document.createElement('ul'))
: '';
document.body.append(createTreeElement(data));
Или, делаем второй параметр строкой. Как и массив из предыдущего варианта, по умолчанию пуста, указывать при самостоятельном вызове не надо. При обработке элемента массива собираем новую строку, состоящую из переданной в функцию и индекса:
const createTreeHTML = (data, index = '') =>
data instanceof Array && data.length
? `<ul>${data.map((n, i) => `
<li>
${(i = index + (index && '_') + i)}
${createTreeHTML(n.children, i)}
</li>`).join('')}
</ul>`
: '';
document.body.insertAdjacentHTML('beforeend', createTreeHTML(data));