diff --git a/packages/docs/.vitepress/config.mts b/packages/docs/.vitepress/config.mts
index 19e8b98..afd65a2 100644
--- a/packages/docs/.vitepress/config.mts
+++ b/packages/docs/.vitepress/config.mts
@@ -136,6 +136,9 @@ export default ({ mode }: { mode: string }) => defineConfigWithTheme({
}, {
text: 'Other helpers',
link: '/guide/other-helpers.html'
+ }, {
+ text: 'With meta',
+ link: '/guide/with-meta.html'
}
]
}, {
diff --git a/packages/docs/src/api.md b/packages/docs/src/api.md
index 28a0ff9..ee45428 100644
--- a/packages/docs/src/api.md
+++ b/packages/docs/src/api.md
@@ -80,6 +80,7 @@ The [`options`](#iterationoptions) object can be used to decide which node types
### See also
* [Guide - Other helpers](/guide/other-helpers.html)
+* [Guide - With meta](/guide/with-meta.html)
## eachChild()
diff --git a/packages/docs/src/guide/iterators.md b/packages/docs/src/guide/iterators.md
index 745ac3f..e892af6 100644
--- a/packages/docs/src/guide/iterators.md
+++ b/packages/docs/src/guide/iterators.md
@@ -91,3 +91,19 @@ For the specific case of counting children, the dedicated [`countChildren()`](./
While these examples need to display the count, a more common scenario involves only needing to know whether the count is 0. The [`isEmpty()`](./other-helpers#checking-for-an-empty-slot) helper can be used in that case.
It is worth noting that the count here is just a count of the VNodes. It is not necessarily an accurate count of the number of `
` elements. If any of the children had been a component it would have added 1 to the count, even though a component wouldn't necessarily render exactly one `` element.
+
+The iteration index and total node count aren't passed to the callback by default. It's relatively easy to implement that yourself:
+
+```js
+const children = slots.default?.() ?? []
+const count = countChildren(children)
+let index = -1
+
+eachChild(children, (vnode) => {
+ index++
+
+ // ...
+})
+```
+
+But if you'd like the index and length to be passed to the callback you can use the [`with-meta`](./with-meta) iterators instead.
diff --git a/packages/docs/src/guide/other-helpers.md b/packages/docs/src/guide/other-helpers.md
index 04854cf..6a45fef 100644
--- a/packages/docs/src/guide/other-helpers.md
+++ b/packages/docs/src/guide/other-helpers.md
@@ -25,6 +25,8 @@ function ChildComponent(_, { slots }) {
See it on the SFC Playground: [Composition API](https://play.vuejs.org/#eNp9VGtv0zAU/StXAamtaJPykJDCxoBp4v0Qm8QHgiBLnNara0e20xVV/e8c22nWdBvaPjS+55577vU93kSv6zpeNSxKoyNTaF5bMsw29ctM8mWttKUNaZYXlq8YbanSakkD4Add/BM39ge38wtlc9EC4qR3Gl8Z4DNZKGksccuWho472uHPTBLKWLa2KQ2+s3IwJjNX1ylZ3aDquBd/qxmTHaLKhbkFeSMgsM+RyV+jGwku8loIiBiO6PglbVx6pTQNbySSqoLUUQiT/4pdKvIcqzsFMf6PkjA7TA0fgNUitwxfREeXjbVK0qtC8GJxnEVt7SzyYaJzR5jjwIGTgA6ZAjOcXGOIE+um2OI7elpNIBmMXi2XQW3H6wmA4VUL8dL3whjYJvTkxkZb9LEL3BK937ofOXje8ZL1BPvMRPCd0KQ/CEQO++lBonEUVmqyzGusjJJYSj961PcBNJfuLiOLXpkF11awBOs4WUlVskljufCgLJpbW5s0SRpZL2ZxoZbJffikhK7Dw5iZ5eRSq2vDdFyyFfRk0e7CofRwvyG188v8bqNsqFCNtKdzLkrN5JjOP77/9vv06+fPZ18uzruc+2R6B7G1pypZlTfCUtVIWAgX5UlPFQpJJu3w9xjVjFDW0LZd37DYRVsc1+jDaM0zncQwwskJ/fy1h3VqAeypHu4YDuR7cxE8bRstaT4clHwFC3pr0833H88TKFN6uPE/tn9G3r+usCt54vCNAHxXbEQpySZ4xPkYl4A7MPavYCYujJt+u7GPCIVCx5dKl0yn9Lhek1GCl/RgOp2+cKFlrmdcYg1rhKf1uj1cYztLO0/pyXR3WOdlyeWsg6FyBiVthbxYzDQ0lyk9qJ67P590R+WiKPYqBzqakpPjKWFVT9mrF8Lo1BpcScVnB67AVtdcMP21dkvQdwdeFHX9wZ+5t6odMHLmrFjccX5l1sE43zTDzq/g8C5mIZrZED47/4LXYi+4VGUjgP5P8DvDFLDESgbYG0wMsvdwXu177xN0f2HO1pZJs2uqe2w9PovgDLfr97V+I/dp/Kzz7PYf9mdPuQ==) | [Options API](https://play.vuejs.org/#eNp9Vftv0zAQ/ldOAamdaJPykJDCYMA08X6ITeIHgiCLncara0f2pSuq+r9zfiRry4Y2qfXdd9/d+e5zN8mrtk1XHU/y5NhWRrT4olBi2WqD8FFY/C6wudBYSqiNXsIozfas6ZUdFQqgUHztYxivy04ibJy10kSkuEKbBwPsczrTdhLiAViJ5fioBxqOnVH9CUAgXxLNj/4MsAHka8xh9I2z0QRso69zQNPxyHmAemM4VwOuLqW9A/hadvyAr0f9DF/82cW6zyXHRrObDl3cKyn7TgossNYGxpVWFn0boGvARtjU9zR03HeZOgZ47lP3nlhBSFwo+j/OhnHRgcJaWSKnE8DxZYeoFbyspKgWz4skllQk3g1w7hKUZHDgLKBDpKTxTK9pPlN0A4r4gR5WU+qFGH0bQoWpDLyegDCijhDfyo6bbnkTenR3DdvhYm8pevcq/LSI561gfK9gH5lJ0Rea7V8EeQ772YMkkyTs+nRZtrTLWpEOwtiig5obJlskL+1CGJQ8I8VMV0ozPu1QSA8qkgaxtXmWdapdzFNa/uwufMaorkNjyu1yemn0teUmZXxF9RRJP3Cq9FB4VGoU6gYa2EaBEikpcnBUulN42gjJDFcTOP/w7uuv0y+fPp19vjgfYu4qk4j+EXbdqQoFDcqTnvYKH/+aUDYrNVrYxpUOG1/F5DRG76bWPNNJShI5OYEfXlQR66ol4F7V457hoPyjoL/4UDTjERMr0m18IYbzb88TKHO4v/Fftr+PovRDyhOH7yTB+2RHkIPqgkZ+UioaAs3A4h/JbVpZd/txYx8AJQodX2rDuMnhYbsGq6VgcG82mz3zz0Rp5kLRGrbknrXraFzTdjJscng0641tyZhQ8wFGmQuqJGYoq8XcUM0sh3v1U/fng27JXFXVTuZABzNw5XhKkqqn3MsX3NQpWhpJLeYHqnBPupDcfGndEuyrg14Uff3e29zbFS+YYhpeLW6xX9l1EM5Xw2nnV6TwwYdUNMfgPjv/TK/FjnOpWScJ/R/nN063QEusVYC9phujsndwvtp3XifU/YU9WyNXtm+qf3zjz1NCynC7flfrN+U+Tp8Mmt3+Bfk4a4M=)
+If you need the child count inside the callback of an iterator then you might prefer to use [`with-meta`](./with-meta) instead, which will pass the count to the iteration callback.
+
If you need more fine-grained control over the counting you can use [`eachChild()` or `reduceChildren()`](./iterators) instead.
## Extracting a single child
diff --git a/packages/docs/src/guide/with-meta.md b/packages/docs/src/guide/with-meta.md
new file mode 100644
index 0000000..aaff2bc
--- /dev/null
+++ b/packages/docs/src/guide/with-meta.md
@@ -0,0 +1,97 @@
+---
+# Updated automatically in production builds: see the VitePress config.
+packageVersions:
+ vue: 3.5.34
+ '@skirtle/vue-vnode-utils': 0.3.0
+---
+# With meta
+
+There are alternative versions of the iterator functions, with added support for accessing extra metadata for the current node.
+
+Currently, this metadata just includes the iteration index and length.
+
+To use this metadata you'll need to import the functions from `@skirtle/vue-vnode-utils/with-meta` rather than `@skirtle/vue-vnode-utils`.
+
+## Accessing meta from callbacks
+
+The meta is passed in an object as the final argument of the callback for each iterator.
+
+The `length` property is a number and should be equivalent to calling [`countChildren()`](./other-helpers#counting-children). It will use the same iteration options as the iterator, so if you're skipping nodes then those nodes won't be included in the `length`. For most iterators, the `length` is the number of times the callback will be called during the iteration.
+
+The `index` is zero-based, counting up to `length - 1`.
+
+The `length` is calculated lazily, so if it isn't accessed the nodes won't be counted.
+
+[`betweenChildren()`](./inserting-new-nodes) behaves a little differently. The `index` will still start at zero, but it will only count up to `length - 2`. The `length` will still reflect the number of nodes, like it would for the other iterators, but that won't match the number of calls to the callback.
+
+## Example
+
+```js
+import { h } from 'vue'
+import { addProps } from '@skirtle/vue-vnode-utils/with-meta'
+
+export default function AddStripes(_, { slots }) {
+ const children = addProps(slots.default(), (vnode, { index, length }) => {
+ return {
+ class: [
+ index % 2 ? 'odd' : 'even',
+ {
+ first: index === 0,
+ last: index === length - 1
+ }
+ ]
+ }
+ })
+
+ return h('div', children)
+}
+```
+
+See it on the SFC Playground: [Composition API](https://play.vuejs.org/#eNqFVFtr2zAU/isHw4gDid3u8uI1u9KHDbaVdW/zGG504qiVJSPJaUrIf98n2bm0tJ0JROf26Vy+o03ysW2zVcdJkZy5uZWtJ8e+a9+VWjatsZ42ZHkxoblp2s6zoC0trGlohKDR3umjEJce0ewGa5YfVNm1g2ep50Y7D6BOe5oF1PTNeKeVnhsH7e6aNB3T7B19q/wya6p1ejIZzlKnESFbVarjCZ2ejMdAOcv77JE3BIC1qvIMiehMakDSatoYwWpWJjG+TMjftQxRd80VW8jAhngSTtUap1Mce4RDLVGGRsgVEBfGBgCS+IUCBv/wbTakabsd3HP491BHfYHiLN+nmkySvpnTpmrRMqMxk02IKQcD4AuKmqD74G6k9YpzDGK60qht2nmpXH4r/XLasK+Ce5ksvW9dkeedbm/qDP3Nn4wU0vlD+ENzxq6ZXllz69hmglfIsUxCNihyi+zvDRy57/mzfJwzG6qEuLCmdXv7/2uKROJ1BBC8qDrladHpuZdGH7Ew/TsBvlPGA3zcN22g31IqYVmDa7vr0+iHkiJcOp5QGu8OEFILXk9Isa496oikHEZgsSdW7yTgq8q5gn7vZOqD6QW9pPc0MkKMqKARr1iPJgenfXz4FtI6XwyBs9mMTo48iXDDPeuQ1pROD14D5Yj+9Icob7Ei4X/IeZmOQMgRtnroBszwwxC9Q5sWsn5AwLCWUrH90YZG3ydipZS5/Rp13mIld/r5kuc3vX5RKXcwXLt1T80Ly+DSistkb/OVrRnLGcznl995jfPeiAXuFLyfMf5kZxQ4Y3Tv9qnTAnkf+cV0v0QKSl3/cudrz9rtqgoVxIZF/zIBET8/U/sh3VfZ66NdcP5OYQ/mLixC1laiD8FB4NICr1a7fht7XuosDn3HUWVsgTEJWAnvkK2lnnrTPggJRLgXUVtmvTeDa731qprf1BbvnShIyXrp7ziUH8EfTybQ84nYKzy4j0cm238hNxSj) | [Options API](https://play.vuejs.org/#eNqFVNtu2zAM/RXCwBAH8CXd5SVLunVDHzZgW7HubR4Gx2JsNbJkSHLqIsi/j/ItTtF2gYGIInl4O9TBu6qqaF+jt/RWJtO8speJ5GWltIUrxm4tXaGBrVYlzKL4dBXdmVkiE4lNa8twm9bCwiGRAJkiAInSmmV3ARMsJx8D5wrAUpv688FGo621HCQHU0u7hHedfJw6ugi1RTbic4ulOUGNYN9SW0Rl2viLoD9z6duCm6hFD+BiMZ9PAySSvlU89oIEgq5EapEkgBWXFBn2YakYinXitTiJB/ahQhJlXW5Qk0yBSFy4U9rQ6YKOHcKpF61MN4zvCXGrtAMATp8rp7d3v8MBJBzbDMk8JvsOajIQuljFk1S9wOvmGJZpRdNSkmbctifpFRRg7F/ifTQ7rq3AmNgQ7iVVF9aWCxPfc1uEJdrUmSdeYW1llnFcy2qXUxfL+FlPxo09uT9WR2jKcKPVvUEdMdxTjok3zICyP+Ma5d6z8gAFHHtCEiKRcFSkjN1oVZlR//+anuLwtpaZ5UpOSOv/DQjfCGUJvGdZpqSxkBVcMI0S1mN4v7Wjklo4fx6A38Z2EFwybAIQKHNLdcxhffks/UVqaIF+DzKR3DnDK3gNH2CmGJvBEma4RzmjtRiMRn/323JtaIU6x/V6DYuJJQBFONP2aYVwcbLqSQfwZ7omtDPuv8+58GdEyVkwdoPUZEdDtIbatOX5IwK6/eUC9Y/KNfqciKkQ6v5re2d1jX3C5FNgtuvut6kwJ8WdaTpq3mgkLu0x8UadTXWOtJ5OfX37HRs6j0pa4VqQ9QvKn2iUIM4o2Zl9qiWjvCd2bbpfWgpymf8y141FaYaqXAXDw+WsiYifX6j9lO6b6O1kF4x9ELQHmXGLEFUp61zowCjokl6xqnnf9jyRUTv0gaNC6SWNiZEW6CXSOZehVdUjF0eEM49cI8pRTVzrtJs02+WaXjx6eQXPC/uArvwW/OlkHD2f8d2IGp/29I7/AH7AK5M=)
+
+## Accessing the `with-meta` iterators
+
+Metadata isn't included in the default iterators, to avoid the added overhead. You'll need to use alternative versions of the iterators to get the metadata.
+
+### With a bundler
+
+If you're using a bundler then you just need to change `@skirtle/vue-vnode-utils` to `@skirtle/vue-vnode-utils/with-meta` in your imports to get access to the metadata:
+
+```js
+import { addProps } from '@skirtle/vue-vnode-utils/with-meta'
+```
+
+All exports from `@skirtle/vue-vnode-utils` are re-exported by `@skirtle/vue-vnode-utils/with-meta`, even those that don't include meta. You can also mix `@skirtle/vue-vnode-utils` and `@skirtle/vue-vnode-utils/with-meta` and functions will be tree-shaken across the two packages.
+
+### CDN - global build
+
+To use meta with a global build, add `with-meta` to the path:
+
+```html-vue
+
+```
+
+The global variable is called `VueVNodeUtils`, the same as with the normal build. You wouldn't load both in the same project.
+
+### CDN - ES module build
+
+Similar to the global build, you need to add `with-meta` to the path:
+
+```html-vue
+
+
+```
+
+The `with-meta` module includes a copy of everything it needs, you don't need to include `@skirtle/vue-vnode-utils` in the import map.
+
+The package name is usually arbitrary, though using `@skirtle/vue-vnode-utils/with-meta` is recommended unless you have a good reason to use an alternative.
diff --git a/packages/vue-vnode-utils/package.json b/packages/vue-vnode-utils/package.json
index 264b0fe..d47e7a2 100644
--- a/packages/vue-vnode-utils/package.json
+++ b/packages/vue-vnode-utils/package.json
@@ -25,6 +25,11 @@
"import": "./dist/vue-vnode-utils.esm-bundler.js",
"require": "./dist/vue-vnode-utils.cjs"
},
+ "./with-meta": {
+ "types": "./dist/with-meta/vue-vnode-utils.d.ts",
+ "import": "./dist/with-meta/vue-vnode-utils.esm-bundler.js",
+ "require": "./dist/with-meta/vue-vnode-utils.cjs"
+ },
"./dist/*": "./dist/*",
"./package.json": "./package.json"
},
diff --git a/packages/vue-vnode-utils/src/__tests__/with-meta.spec.ts b/packages/vue-vnode-utils/src/__tests__/with-meta.spec.ts
new file mode 100644
index 0000000..87c0eda
--- /dev/null
+++ b/packages/vue-vnode-utils/src/__tests__/with-meta.spec.ts
@@ -0,0 +1,499 @@
+import { describe, expect, it } from 'vitest'
+import { h, isVNode, type VNode } from 'vue'
+import {
+ addProps,
+ ALL_VNODES,
+ betweenChildren,
+ COMPONENTS_AND_ELEMENTS,
+ eachChild,
+ everyChild,
+ findChild,
+ getText,
+ isElement,
+ isText,
+ reduceChildren,
+ replaceChildren,
+ someChild
+} from '../with-meta'
+
+function getAllKeys(obj: object) {
+ const keys: string[] = []
+
+ for (const key in obj) {
+ keys.push(key)
+ }
+
+ return keys
+}
+
+describe('someChild', () => {
+ it('someChild - 39f9', () => {
+ const startNodes = [h('div'), 'Some text', h('p')]
+
+ let count = 0
+
+ let out = someChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+ return false
+ })
+
+ expect(out).toBe(false)
+ expect(count).toBe(3)
+
+ count = 0
+
+ out = someChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(2)
+ count++
+ return false
+ }, COMPONENTS_AND_ELEMENTS)
+
+ expect(out).toBe(false)
+ expect(count).toBe(2)
+
+ count = 0
+
+ out = someChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+ count++
+ return count === 2
+ })
+
+ expect(out).toBe(true)
+ expect(count).toBe(2)
+ })
+})
+
+describe('everyChild', () => {
+ it('everyChild - 1d78', () => {
+ const startNodes = [h('div'), 'Some text', h('p')]
+
+ let count = 0
+
+ let out = everyChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+ return true
+ })
+
+ expect(out).toBe(true)
+ expect(count).toBe(3)
+
+ count = 0
+
+ out = everyChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(2)
+ count++
+ return true
+ }, COMPONENTS_AND_ELEMENTS)
+
+ expect(out).toBe(true)
+ expect(count).toBe(2)
+
+ count = 0
+
+ out = everyChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+ count++
+ return count !== 2
+ })
+
+ expect(out).toBe(false)
+ expect(count).toBe(2)
+ })
+})
+
+describe('eachChild', () => {
+ it('eachChild - 2558', () => {
+ const startNodes = [h('div'), 'Some text', h('p')]
+
+ let count = 0
+
+ let out = eachChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+ return !(count % 2)
+ })
+
+ expect(out).toBeUndefined()
+ expect(count).toBe(3)
+
+ count = 0
+
+ out = eachChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(2)
+ count++
+ return count % 2
+ }, COMPONENTS_AND_ELEMENTS)
+
+ expect(out).toBeUndefined()
+ expect(count).toBe(2)
+ })
+})
+
+describe('findChild', () => {
+ it('findChild - abc2', () => {
+ const startNodes = [h('div'), 'Some text', h('p')]
+
+ let count = 0
+
+ let out = findChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+ return false
+ })
+
+ expect(out).toBeUndefined()
+ expect(count).toBe(3)
+
+ count = 0
+
+ out = findChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(2)
+ count++
+ return false
+ }, COMPONENTS_AND_ELEMENTS)
+
+ expect(out).toBeUndefined()
+ expect(count).toBe(2)
+
+ count = 0
+
+ out = findChild(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+ count++
+ return count === 2
+ })
+
+ expect(isText(out)).toBe(true)
+ expect(count).toBe(2)
+ })
+})
+
+describe('reduceChildren', () => {
+ it('reduceChildren - 0ddb', () => {
+ const startNodes = ['1', '2', '3']
+
+ let count = 0
+
+ let out = reduceChildren(startNodes, (acc, node, meta) => {
+ expect(acc).toBe([0, 1, 3][count])
+ expect(isVNode(node)).toBe(true)
+ expect(getText(node)).toBe(String(count + 1))
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+ return acc + count
+ }, 0)
+
+ expect(out).toBe(6)
+ expect(count).toBe(3)
+
+ count = 0
+
+ out = reduceChildren([h('div'), 'Some text', h('p')], (acc, node, meta) => {
+ expect(acc).toBe([0, 1][count])
+ expect(isVNode(node)).toBe(true)
+ expect(isText(node)).toBe(false)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(2)
+
+ count++
+ return acc + count
+ }, 0, COMPONENTS_AND_ELEMENTS)
+
+ expect(out).toBe(3)
+ expect(count).toBe(2)
+ })
+})
+
+describe('addProps', () => {
+ it('addProps - 5ad7', () => {
+ const startNodes = [h('div'), 'Some text', h('p')]
+
+ let count = 0
+
+ const nodes = addProps(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(isElement(node)).toBe(true)
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(2)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+
+ return {
+ class: 'red'
+ }
+ })
+
+ expect(Array.isArray(nodes)).toBe(true)
+ expect(nodes).toHaveLength(3)
+ expect(count).toBe(2)
+
+ const node = nodes[0] as VNode
+
+ expect(isElement(node)).toBe(true)
+ expect(node.type).toBe('div')
+ expect(node.props?.class).toBe('red')
+
+ count = 0
+
+ const unchanged = addProps(startNodes, () => {
+ ++count
+ }, { component: true })
+
+ expect(count).toBe(0)
+ expect(unchanged).toBe(startNodes)
+ })
+})
+
+describe('replaceChildren', () => {
+ it('replaceChildren - 9c3c', () => {
+ const startNodes = [h('div'), 'Some text', h('p')]
+
+ let count = 0
+
+ let nodes = replaceChildren(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+
+ if (count === 2) {
+ return []
+ }
+ })
+
+ expect(Array.isArray(nodes)).toBe(true)
+ expect(nodes).toHaveLength(2)
+ expect(count).toBe(3)
+
+ let node = nodes[0] as VNode
+ expect(isElement(node)).toBe(true)
+ expect(node.type).toBe('div')
+
+ node = nodes[1] as VNode
+ expect(isElement(node)).toBe(true)
+ expect(node.type).toBe('p')
+
+ count = 0
+
+ nodes = replaceChildren(startNodes, (node, meta) => {
+ expect(isVNode(node)).toBe(true)
+ expect(isElement(node)).toBe(true)
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(2)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+
+ if (count === 2) {
+ return []
+ }
+ }, COMPONENTS_AND_ELEMENTS)
+
+ expect(Array.isArray(nodes)).toBe(true)
+ expect(nodes).toHaveLength(2)
+ expect(count).toBe(2)
+
+ node = nodes[0] as VNode
+ expect(isElement(node)).toBe(true)
+ expect(node.type).toBe('div')
+
+ node = nodes[1] as VNode
+ expect(isText(node)).toBe(true)
+ expect(getText(node)).toBe('Some text')
+
+ count = 0
+
+ nodes = replaceChildren([true, false, null], () => {
+ ++count
+ })
+
+ expect(count).toBe(0)
+ expect(Array.isArray(nodes)).toBe(true)
+ expect(nodes).toHaveLength(3)
+ })
+})
+
+describe('betweenChildren', () => {
+ it('betweenChildren - 719e', () => {
+ const startNodes = [h('div'), 'Some text', h('p')]
+
+ let count = 0
+
+ let nodes = betweenChildren(startNodes, (prev, next, meta) => {
+ expect(isVNode(prev)).toBe(true)
+ expect(isVNode(next)).toBe(true)
+
+ let metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ expect(meta.index).toBe(count)
+ expect(meta.length).toBe(3)
+
+ // `length` property should still be enumerable
+ metaProperties = getAllKeys(meta)
+ expect(metaProperties.includes('index')).toBe(true)
+ expect(metaProperties.includes('length')).toBe(true)
+
+ count++
+
+ return String(count)
+ })
+
+ expect(Array.isArray(nodes)).toBe(true)
+ expect(nodes).toHaveLength(5)
+ expect(count).toBe(2)
+
+ let node = nodes[0] as VNode
+ expect(isElement(node)).toBe(true)
+ expect(node.type).toBe('div')
+
+ node = nodes[1] as VNode
+ expect(isText(node)).toBe(true)
+ expect(getText(node)).toBe('1')
+
+ node = nodes[2] as VNode
+ expect(isText(node)).toBe(true)
+ expect(getText(node)).toBe('Some text')
+
+ node = nodes[3] as VNode
+ expect(isText(node)).toBe(true)
+ expect(getText(node)).toBe('2')
+
+ node = nodes[4] as VNode
+ expect(isElement(node)).toBe(true)
+ expect(node.type).toBe('p')
+
+ count = 0
+
+ const comments = [true, false, null]
+
+ nodes = betweenChildren(comments, () => {
+ ++count
+ })
+
+ expect(count).toBe(0)
+ expect(Array.isArray(nodes)).toBe(true)
+ expect(nodes).toHaveLength(3)
+
+ nodes = betweenChildren(comments, () => {
+ ++count
+ }, ALL_VNODES)
+
+ expect(count).toBe(2)
+ expect(Array.isArray(nodes)).toBe(true)
+ expect(nodes).toHaveLength(3)
+ })
+})
diff --git a/packages/vue-vnode-utils/src/iterators.ts b/packages/vue-vnode-utils/src/iterators.ts
index b0d8fce..e3d55e7 100644
--- a/packages/vue-vnode-utils/src/iterators.ts
+++ b/packages/vue-vnode-utils/src/iterators.ts
@@ -20,7 +20,7 @@ const warn = (method: string, msg: string) => {
console.warn(`[${method}] ${msg}`)
}
-const checkArguments = (method: string, passed: unknown[], expected: string[]) => {
+export const checkArguments = (method: string, passed: unknown[], expected: string[]) => {
for (let index = 0; index < passed.length; ++index) {
const t = typeOf(passed[index])
const expect = expected[index]
diff --git a/packages/vue-vnode-utils/src/with-meta/index.ts b/packages/vue-vnode-utils/src/with-meta/index.ts
new file mode 100644
index 0000000..e8ede77
--- /dev/null
+++ b/packages/vue-vnode-utils/src/with-meta/index.ts
@@ -0,0 +1,31 @@
+export {
+ ALL_VNODES,
+ COMPONENTS_AND_ELEMENTS,
+ countChildren,
+ extractSingleChild,
+ getText,
+ getType,
+ isComment,
+ isComponent,
+ isElement,
+ isEmpty,
+ isFragment,
+ isFunctionalComponent,
+ isStatefulComponent,
+ isStatic,
+ isText,
+ type IterationOptions,
+ SKIP_COMMENTS
+} from '@skirtle/vue-vnode-utils'
+
+export {
+ addProps,
+ betweenChildren,
+ eachChild,
+ findChild,
+ everyChild,
+ type IterationMeta,
+ reduceChildren,
+ replaceChildren,
+ someChild
+} from './iterators'
diff --git a/packages/vue-vnode-utils/src/with-meta/iterators.ts b/packages/vue-vnode-utils/src/with-meta/iterators.ts
new file mode 100644
index 0000000..8b1e09e
--- /dev/null
+++ b/packages/vue-vnode-utils/src/with-meta/iterators.ts
@@ -0,0 +1,172 @@
+import type { VNode, VNodeArrayChildren } from 'vue'
+
+import type { IterationOptions } from '@skirtle/vue-vnode-utils'
+import {
+ addProps as addPropsRaw,
+ ALL_VNODES,
+ betweenChildren as betweenChildrenRaw,
+ COMPONENTS_AND_ELEMENTS,
+ countChildren,
+ eachChild as eachChildRaw,
+ everyChild as everyChildRaw,
+ findChild as findChildRaw,
+ reduceChildren as reduceChildrenRaw,
+ replaceChildren as replaceChildrenRaw,
+ SKIP_COMMENTS,
+ someChild as someChildRaw
+} from '@skirtle/vue-vnode-utils'
+
+// TODO: Should probably move `checkArguments` elsewhere
+import { checkArguments } from '../iterators'
+
+type AnyFunction = (...args: never) => unknown
+
+function checkFunction(method: string, callback: AnyFunction) {
+ checkArguments(method, [callback], ['function'])
+}
+
+export type IterationMeta = {
+ readonly index: number
+ readonly length: number
+}
+
+function setPropertyValue(obj: object, key: string, value: unknown): any {
+ return Object.defineProperty(obj, key, {
+ value,
+ enumerable: true
+ })
+}
+
+function createMetaFactory(children: VNodeArrayChildren, options: IterationOptions): () => IterationMeta {
+ let index = -1
+
+ const baseMeta = {
+ get length() {
+ const length = countChildren(children, options)
+ setPropertyValue(baseMeta, 'length', length)
+ return length
+ }
+ }
+
+ return () => {
+ return setPropertyValue(Object.create(baseMeta), 'index', ++index)
+ }
+}
+
+function withMeta(
+ iterator: (children: VNodeArrayChildren, callback: (vnode: VNode) => CallbackReturn, options: IterationOptions) => IteratorReturn,
+ children: VNodeArrayChildren,
+ callback: (vnode: VNode, meta: IterationMeta) => CallbackReturn,
+ options: IterationOptions
+): IteratorReturn {
+ const metaFactory = createMetaFactory(children, options)
+
+ return iterator(children, (vnode: VNode) => {
+ return callback(vnode, metaFactory())
+ }, options)
+}
+
+export const addProps = (
+ children: VNodeArrayChildren,
+ callback: (vnode: VNode, meta: IterationMeta) => (Record | null | void),
+ options: IterationOptions = COMPONENTS_AND_ELEMENTS
+): VNodeArrayChildren => {
+ if (__DEV__) {
+ checkFunction('addProps', callback)
+ }
+
+ return withMeta(addPropsRaw, children, callback, options)
+}
+
+export const replaceChildren = (
+ children: VNodeArrayChildren,
+ callback: (vnode: VNode, meta: IterationMeta) => (VNode | VNodeArrayChildren | string | number | void),
+ options: IterationOptions = SKIP_COMMENTS
+): VNodeArrayChildren => {
+ if (__DEV__) {
+ checkFunction('replaceChildren', callback)
+ }
+
+ return withMeta(replaceChildrenRaw, children, callback, options)
+}
+
+export const betweenChildren = (
+ children: VNodeArrayChildren,
+ callback: (previousVNode: VNode, nextVNode: VNode, meta: IterationMeta) => (VNode | VNodeArrayChildren | string | number | void),
+ options: IterationOptions = SKIP_COMMENTS
+): VNodeArrayChildren => {
+ if (__DEV__) {
+ checkFunction('betweenChildren', callback)
+ }
+
+ const metaFactory = createMetaFactory(children, options)
+
+ return betweenChildrenRaw(children, (previousVNode: VNode, nextVNode: VNode) => {
+ return callback(previousVNode, nextVNode, metaFactory())
+ }, options)
+}
+
+export const someChild = (
+ children: VNodeArrayChildren,
+ callback: (vnode: VNode, meta: IterationMeta) => unknown,
+ options: IterationOptions = ALL_VNODES
+): boolean => {
+ if (__DEV__) {
+ checkFunction('someChild', callback)
+ }
+
+ return withMeta(someChildRaw, children, callback, options)
+}
+
+export const everyChild = (
+ children: VNodeArrayChildren,
+ callback: (vnode: VNode, meta: IterationMeta) => unknown,
+ options: IterationOptions = ALL_VNODES
+): boolean => {
+ if (__DEV__) {
+ checkFunction('everyChild', callback)
+ }
+
+ return withMeta(everyChildRaw, children, callback, options)
+}
+
+export const eachChild = (
+ children: VNodeArrayChildren,
+ callback: (vnode: VNode, meta: IterationMeta) => void,
+ options: IterationOptions = ALL_VNODES
+): void => {
+ if (__DEV__) {
+ checkFunction('eachChild', callback)
+ }
+
+ return withMeta(eachChildRaw, children, callback, options)
+}
+
+export const findChild = (
+ children: VNodeArrayChildren,
+ callback: (vnode: VNode, meta: IterationMeta) => unknown,
+ options: IterationOptions = ALL_VNODES
+): (VNode | undefined) => {
+ if (__DEV__) {
+ checkFunction('findChild', callback)
+ }
+
+ return withMeta(findChildRaw, children, callback, options)
+}
+
+export const reduceChildren = (
+ children: VNodeArrayChildren,
+ callback: (previousValue: R, vnode: VNode, meta: IterationMeta) => R,
+ initialValue: R,
+ options: IterationOptions = ALL_VNODES
+): R => {
+ if (__DEV__) {
+ checkFunction('reduceChildren', callback)
+ }
+
+ const metaFactory = createMetaFactory(children, options)
+
+ return reduceChildrenRaw(children, (previousValue: R, vnode: VNode) => {
+ return callback(previousValue, vnode, metaFactory())
+ }, initialValue, options)
+}
diff --git a/packages/vue-vnode-utils/tsconfig.app.json b/packages/vue-vnode-utils/tsconfig.app.json
index 02a220a..0e7edcc 100644
--- a/packages/vue-vnode-utils/tsconfig.app.json
+++ b/packages/vue-vnode-utils/tsconfig.app.json
@@ -3,6 +3,9 @@
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "paths": {
+ "@skirtle/vue-vnode-utils": ["./src/index"]
+ }
}
}
diff --git a/packages/vue-vnode-utils/tsdown.config.ts b/packages/vue-vnode-utils/tsdown.config.ts
index 37c8420..e45ee9a 100644
--- a/packages/vue-vnode-utils/tsdown.config.ts
+++ b/packages/vue-vnode-utils/tsdown.config.ts
@@ -1,4 +1,5 @@
import { defineConfig } from 'tsdown'
+import { fileURLToPath, URL } from 'node:url'
const base = defineConfig({
entry: {
@@ -67,11 +68,87 @@ const iifeProd = defineConfig({
...suffix('global.prod.js')
})
+const withMetaBase = defineConfig({
+ ...base,
+ outDir: 'dist/with-meta',
+ entry: {
+ 'vue-vnode-utils': 'src/with-meta/index.ts'
+ },
+ alias: {
+ '@skirtle/vue-vnode-utils': fileURLToPath(new URL('./src', import.meta.url))
+ }
+})
+
+const withMetaCjs = defineConfig({
+ ...withMetaBase,
+ format: ['cjs'],
+ define: {
+ __DEV__: `!(process.env.NODE_ENV === 'production')`
+ },
+ alias: {},
+ deps: {
+ neverBundle: [
+ 'vue',
+ '@skirtle/vue-vnode-utils'
+ ]
+ }
+})
+
+const withMetaEsmBundler = defineConfig({
+ ...withMetaCjs,
+ format: ['esm'],
+ dts: { tsconfig: './tsconfig.app.json' },
+ outExtensions: () => ({ js: '.esm-bundler.js', dts: '.d.ts' }),
+ deps: {
+ neverBundle: [
+ 'vue',
+ '@skirtle/vue-vnode-utils'
+ ]
+ }
+})
+
+const withMetaEsmBrowserDev = defineConfig({
+ ...withMetaBase,
+ format: ['esm'],
+ define: {
+ __DEV__: 'true'
+ },
+ ...suffix('esm-browser.dev.js')
+})
+
+const withMetaIifeDev = defineConfig({
+ ...withMetaEsmBrowserDev,
+ format: ['iife'],
+ ...suffix('global.dev.js')
+})
+
+const withMetaEsmBrowserProd = defineConfig({
+ ...withMetaBase,
+ format: ['esm'],
+ minify: true,
+ define: {
+ __DEV__: 'false'
+ },
+ ...suffix('esm-browser.prod.js')
+})
+
+const withMetaIifeProd = defineConfig({
+ ...withMetaEsmBrowserProd,
+ format: ['iife'],
+ ...suffix('global.prod.js')
+})
+
export default [
cjs,
esmBundler,
esmBrowserDev,
iifeDev,
esmBrowserProd,
- iifeProd
+ iifeProd,
+ withMetaCjs,
+ withMetaEsmBundler,
+ withMetaEsmBrowserDev,
+ withMetaIifeDev,
+ withMetaEsmBrowserProd,
+ withMetaIifeProd
]
diff --git a/packages/vue-vnode-utils/vite.config.mts b/packages/vue-vnode-utils/vite.config.mts
index ccd4d6a..791094a 100644
--- a/packages/vue-vnode-utils/vite.config.mts
+++ b/packages/vue-vnode-utils/vite.config.mts
@@ -34,6 +34,13 @@ export default defineConfig(({ mode }): UserConfig => {
dtsPlugin
],
+ resolve: {
+ alias: {
+ // Alias so `with-meta` can import from the root package
+ '@skirtle/vue-vnode-utils': fileURLToPath(new URL('./src/index.ts', import.meta.url))
+ }
+ },
+
build: {
target: 'es2019',
emptyOutDir: false,