Pārlūkot izejas kodu

workflow: add quick benchmark (#266)

Kevin Deng 三咲智子 1 gadu atpakaļ
vecāks
revīzija
25f8502546

+ 1 - 0
benchmark/.gitignore

@@ -0,0 +1 @@
+results/*

+ 6 - 4
playground/src/bench/App.vue → benchmark/client/App.vue

@@ -1,8 +1,10 @@
-<script setup lang="ts">
-import { ref, shallowRef } from 'vue'
+<script setup lang="ts" vapor>
+import { ref, shallowRef } from '@vue/vapor'
 import { buildData } from './data'
 import { buildData } from './data'
 import { defer, wrap } from './profiling'
 import { defer, wrap } from './profiling'
 
 
+const isVapor = !!import.meta.env.IS_VAPOR
+
 const selected = ref<number>()
 const selected = ref<number>()
 const rows = shallowRef<
 const rows = shallowRef<
   {
   {
@@ -75,14 +77,14 @@ async function bench() {
 </script>
 </script>
 
 
 <template>
 <template>
-  <h1>Vue.js Vapor Benchmark</h1>
+  <h1>Vue.js ({{ isVapor ? 'Vapor' : 'Virtual DOM' }}) Benchmark</h1>
   <div
   <div
     id="control"
     id="control"
     style="display: flex; flex-direction: column; width: fit-content; gap: 6px"
     style="display: flex; flex-direction: column; width: fit-content; gap: 6px"
   >
   >
     <button @click="bench">Benchmark mounting</button>
     <button @click="bench">Benchmark mounting</button>
     <button id="run" @click="run">Create 1,000 rows</button>
     <button id="run" @click="run">Create 1,000 rows</button>
-    <button id="runlots" @click="runLots">Create 10,000 rows</button>
+    <button id="runLots" @click="runLots">Create 10,000 rows</button>
     <button id="add" @click="add">Append 1,000 rows</button>
     <button id="add" @click="add">Append 1,000 rows</button>
     <button id="update" @click="update">Update every 10th row</button>
     <button id="update" @click="update">Update every 10th row</button>
     <button id="clear" @click="clear">Clear</button>
     <button id="clear" @click="clear">Clear</button>

+ 0 - 0
playground/src/bench/data.ts → benchmark/client/data.ts


+ 17 - 0
benchmark/client/index.html

@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vue Vapor Benchmark</title>
+    <style>
+      html {
+        color-scheme: light dark;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="./index.ts"></script>
+  </body>
+</html>

+ 5 - 0
benchmark/client/index.ts

@@ -0,0 +1,5 @@
+if (import.meta.env.IS_VAPOR) {
+  import('./vapor')
+} else {
+  import('./vdom')
+}

+ 79 - 0
benchmark/client/profiling.ts

@@ -0,0 +1,79 @@
+/* eslint-disable no-console */
+/* eslint-disable no-restricted-syntax */
+/* eslint-disable no-restricted-globals */
+
+declare module globalThis {
+  let doProfile: boolean
+  let recordTime: boolean
+  let times: Record<string, number[]>
+}
+
+globalThis.recordTime = true
+globalThis.doProfile = false
+
+export const defer = () => new Promise(r => requestIdleCallback(r))
+
+const times: Record<string, number[]> = (globalThis.times = {})
+
+export function wrap(
+  id: string,
+  fn: (...args: any[]) => any,
+): (...args: any[]) => Promise<void> {
+  return async (...args) => {
+    if (!globalThis.recordTime) {
+      return fn(...args)
+    }
+
+    document.body.classList.remove('done')
+
+    const { doProfile } = globalThis
+    await defer()
+
+    doProfile && console.profile(id)
+    const start = performance.now()
+    fn(...args)
+
+    await defer()
+    const time = performance.now() - start
+    const prevTimes = times[id] || (times[id] = [])
+    prevTimes.push(time)
+
+    const { min, max, median, mean, std } = compute(prevTimes)
+    const msg =
+      `${id}: min: ${min} / ` +
+      `max: ${max} / ` +
+      `median: ${median}ms / ` +
+      `mean: ${mean}ms / ` +
+      `time: ${time.toFixed(2)}ms / ` +
+      `std: ${std} ` +
+      `over ${prevTimes.length} runs`
+    doProfile && console.profileEnd(id)
+    console.log(msg)
+    const timeEl = document.getElementById('time')!
+    timeEl.textContent = msg
+
+    document.body.classList.add('done')
+  }
+}
+
+function compute(array: number[]) {
+  const n = array.length
+  const max = Math.max(...array)
+  const min = Math.min(...array)
+  const mean = array.reduce((a, b) => a + b) / n
+  const std = Math.sqrt(
+    array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
+  )
+  const median = array.slice().sort((a, b) => a - b)[Math.floor(n / 2)]
+  return {
+    max: round(max),
+    min: round(min),
+    mean: round(mean),
+    std: round(std),
+    median: round(median),
+  }
+}
+
+function round(n: number) {
+  return +n.toFixed(2)
+}

+ 4 - 0
benchmark/client/vapor.ts

@@ -0,0 +1,4 @@
+import { createVaporApp } from '@vue/vapor'
+import App from './App.vue'
+
+createVaporApp(App as any).mount('#app')

+ 4 - 0
benchmark/client/vdom.ts

@@ -0,0 +1,4 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+
+createApp(App).mount('#app')

+ 340 - 0
benchmark/index.js

@@ -0,0 +1,340 @@
+// @ts-check
+import path from 'node:path'
+import { parseArgs } from 'node:util'
+import { mkdir, rm, writeFile } from 'node:fs/promises'
+import Vue from '@vitejs/plugin-vue'
+import { build } from 'vite'
+import connect from 'connect'
+import sirv from 'sirv'
+import { launch } from 'puppeteer'
+import colors from 'picocolors'
+import { exec, getSha } from '../scripts/utils.js'
+
+// Thanks to https://github.com/krausest/js-framework-benchmark (Apache-2.0 license)
+const {
+  values: {
+    skipLib,
+    skipApp,
+    skipBench,
+    vdom,
+    noVapor,
+    port: portStr,
+    count: countStr,
+    noHeadless,
+  },
+} = parseArgs({
+  allowNegative: true,
+  allowPositionals: true,
+  options: {
+    skipLib: {
+      type: 'boolean',
+      short: 'v',
+    },
+    skipApp: {
+      type: 'boolean',
+      short: 'a',
+    },
+    skipBench: {
+      type: 'boolean',
+      short: 'b',
+    },
+    noVapor: {
+      type: 'boolean',
+    },
+    vdom: {
+      type: 'boolean',
+      short: 'v',
+    },
+    port: {
+      type: 'string',
+      short: 'p',
+      default: '8193',
+    },
+    count: {
+      type: 'string',
+      short: 'c',
+      default: '50',
+    },
+    noHeadless: {
+      type: 'boolean',
+    },
+  },
+})
+
+const port = +(/** @type {string}*/ (portStr))
+const count = +(/** @type {string}*/ (countStr))
+const sha = await getSha(true)
+
+if (!skipLib) {
+  await buildLib()
+}
+if (!skipApp) {
+  await rm('client/dist', { recursive: true }).catch(() => {})
+  vdom && (await buildApp(false))
+  !noVapor && (await buildApp(true))
+}
+const server = startServer()
+
+if (!skipBench) {
+  await benchmark()
+  server.close()
+}
+
+async function buildLib() {
+  console.info(colors.blue('Building lib...'))
+
+  const options = {
+    cwd: path.resolve(import.meta.dirname, '..'),
+    stdio: 'inherit',
+  }
+  const [{ ok }, { ok: ok2 }, { ok: ok3 }, { ok: ok4 }] = await Promise.all([
+    exec(
+      'pnpm',
+      'run --silent build shared compiler-core compiler-dom compiler-vapor -pf cjs'.split(
+        ' ',
+      ),
+      options,
+    ),
+    exec(
+      'pnpm',
+      'run --silent build compiler-sfc compiler-ssr -f cjs'.split(' '),
+      options,
+    ),
+    exec(
+      'pnpm',
+      'run --silent build vue-vapor -pf esm-browser'.split(' '),
+      options,
+    ),
+    exec(
+      'pnpm',
+      'run --silent build vue -pf esm-browser-runtime'.split(' '),
+      options,
+    ),
+  ])
+
+  if (!ok || !ok2 || !ok3 || !ok4) {
+    console.error('Failed to build')
+    process.exit(1)
+  }
+}
+
+/** @param {boolean} isVapor */
+async function buildApp(isVapor) {
+  console.info(
+    colors.blue(`\nBuilding ${isVapor ? 'Vapor' : 'Virtual DOM'} app...\n`),
+  )
+
+  process.env.NODE_ENV = 'production'
+  const CompilerSFC = await import(
+    '../packages/compiler-sfc/dist/compiler-sfc.cjs.js'
+  )
+  /** @type {any} */
+  const TemplateCompiler = await import(
+    isVapor
+      ? '../packages/compiler-vapor/dist/compiler-vapor.cjs.prod.js'
+      : '../packages/compiler-dom/dist/compiler-dom.cjs.prod.js'
+  )
+  const runtimePath = path.resolve(
+    import.meta.dirname,
+    isVapor
+      ? '../packages/vue-vapor/dist/vue-vapor.esm-browser.prod.js'
+      : '../packages/vue/dist/vue.runtime.esm-browser.prod.js',
+  )
+
+  const mode = isVapor ? 'vapor' : 'vdom'
+  await build({
+    root: './client',
+    base: `/${mode}`,
+    define: {
+      'import.meta.env.IS_VAPOR': String(isVapor),
+    },
+    build: {
+      minify: 'terser',
+      outDir: path.resolve('./client/dist', mode),
+      rollupOptions: {
+        onwarn(log, handler) {
+          if (log.code === 'INVALID_ANNOTATION') return
+          handler(log)
+        },
+      },
+    },
+    resolve: {
+      alias: {
+        '@vue/vapor': runtimePath,
+        'vue/vapor': runtimePath,
+        vue: runtimePath,
+      },
+    },
+    clearScreen: false,
+    plugins: [
+      Vue({
+        compiler: CompilerSFC,
+        template: { compiler: TemplateCompiler },
+      }),
+    ],
+  })
+}
+
+function startServer() {
+  const server = connect().use(sirv('./client/dist')).listen(port)
+  console.info(`\n\nServer started at`, colors.blue(`http://localhost:${port}`))
+  process.on('SIGTERM', () => server.close())
+  return server
+}
+
+async function benchmark() {
+  console.info(colors.blue(`\nStarting benchmark...`))
+
+  const browser = await initBrowser()
+
+  await mkdir('results', { recursive: true }).catch(() => {})
+  if (!noVapor) {
+    await doBench(browser, true)
+  }
+  if (vdom) {
+    await doBench(browser, false)
+  }
+
+  await browser.close()
+}
+
+/**
+ *
+ * @param {import('puppeteer').Browser} browser
+ * @param {boolean} isVapor
+ */
+async function doBench(browser, isVapor) {
+  const mode = isVapor ? 'vapor' : 'vdom'
+  console.info('\n\nmode:', mode)
+
+  const page = await browser.newPage()
+  await page.goto(`http://localhost:${port}/${mode}`, {
+    waitUntil: 'networkidle0',
+  })
+
+  await forceGC()
+  const t = performance.now()
+
+  for (let i = 0; i < count; i++) {
+    await clickButton('run') // test: create rows
+    await clickButton('update') // partial update
+    await clickButton('swaprows') // swap rows
+    await select() // test: select row, remove row
+    await clickButton('clear') // clear rows
+
+    await withoutRecord(() => clickButton('run'))
+    await clickButton('add') // append rows to large table
+
+    await withoutRecord(() => clickButton('clear'))
+    await clickButton('runLots') // create many rows
+    await withoutRecord(() => clickButton('clear'))
+
+    // TODO replace all rows
+  }
+
+  console.info(
+    'Total time:',
+    colors.cyan(((performance.now() - t) / 1000).toFixed(2)),
+    's',
+  )
+  const times = await getTimes()
+  const result =
+    /** @type {Record<string, typeof compute>} */
+    Object.fromEntries(Object.entries(times).map(([k, v]) => [k, compute(v)]))
+
+  console.table(result)
+  await writeFile(
+    `results/benchmark-${sha}-${mode}.json`,
+    JSON.stringify(result, undefined, 2),
+  )
+  await page.close()
+  return result
+
+  function getTimes() {
+    return page.evaluate(() => /** @type {any} */ (globalThis).times)
+  }
+
+  async function forceGC() {
+    await page.evaluate(
+      `window.gc({type:'major',execution:'sync',flavor:'last-resort'})`,
+    )
+  }
+
+  /** @param {() => any} fn */
+  async function withoutRecord(fn) {
+    await page.evaluate(() => (globalThis.recordTime = false))
+    await fn()
+    await page.evaluate(() => (globalThis.recordTime = true))
+  }
+
+  /** @param {string} id */
+  async function clickButton(id) {
+    await page.click(`#${id}`)
+    await wait()
+  }
+
+  async function select() {
+    for (let i = 1; i <= 10; i++) {
+      await page.click(`tbody > tr:nth-child(2) > td:nth-child(2) > a`)
+      await page.waitForSelector(`tbody > tr:nth-child(2).danger`)
+      await page.click(`tbody > tr:nth-child(2) > td:nth-child(3) > button`)
+      await wait()
+    }
+  }
+
+  async function wait() {
+    await page.waitForSelector('.done')
+  }
+}
+
+async function initBrowser() {
+  const disableFeatures = [
+    'Translate', // avoid translation popups
+    'PrivacySandboxSettings4', // avoid privacy popup
+    'IPH_SidePanelGenericMenuFeature', // bookmark popup see https://github.com/krausest/js-framework-benchmark/issues/1688
+  ]
+
+  const args = [
+    '--js-flags=--expose-gc', // needed for gc() function
+    '--no-default-browser-check',
+    '--disable-sync',
+    '--no-first-run',
+    '--ash-no-nudges',
+    '--disable-extensions',
+    `--disable-features=${disableFeatures.join(',')}`,
+  ]
+
+  const headless = !noHeadless
+  console.info('headless:', headless)
+  const browser = await launch({
+    headless: headless,
+    args,
+  })
+  console.log('browser version:', colors.blue(await browser.version()))
+
+  return browser
+}
+
+/** @param {number[]} array */
+function compute(array) {
+  const n = array.length
+  const max = Math.max(...array)
+  const min = Math.min(...array)
+  const mean = array.reduce((a, b) => a + b) / n
+  const std = Math.sqrt(
+    array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
+  )
+  const median = array.slice().sort((a, b) => a - b)[Math.floor(n / 2)]
+  return {
+    max: round(max),
+    min: round(min),
+    mean: round(mean),
+    std: round(std),
+    median: round(median),
+  }
+}
+
+/** @param {number} n */
+function round(n) {
+  return +n.toFixed(2)
+}

+ 19 - 0
benchmark/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "benchmark",
+  "version": "0.0.0",
+  "author": "三咲智子 Kevin Deng <sxzz@sxzz.moe>",
+  "license": "MIT",
+  "type": "module",
+  "scripts": {
+    "start": "node index.js"
+  },
+  "dependencies": {
+    "@vitejs/plugin-vue": "npm:@vue-vapor/vite-plugin-vue@0.0.0-alpha.6",
+    "connect": "^3.7.0",
+    "sirv": "^2.0.4",
+    "vite": "^5.0.12"
+  },
+  "devDependencies": {
+    "@types/connect": "^3.4.38"
+  }
+}

+ 26 - 0
benchmark/tsconfig.json

@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "lib": ["es2022", "dom"],
+    "allowJs": true,
+    "moduleDetection": "force",
+    "module": "preserve",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "types": ["node", "vite/client"],
+    "strict": true,
+    "noUnusedLocals": true,
+    "declaration": true,
+    "esModuleInterop": true,
+    "isolatedModules": true,
+    "verbatimModuleSyntax": true,
+    "skipLibCheck": true,
+    "noEmit": true,
+    "paths": {
+      "vue": ["../packages/vue/src"],
+      "@vue/vapor": ["../packages/vue-vapor/src"],
+      "@vue/*": ["../packages/*/src"]
+    }
+  },
+  "include": ["**/*"]
+}

+ 1 - 0
eslint.config.js

@@ -146,6 +146,7 @@ export default tseslint.config(
       'eslint.config.js',
       'eslint.config.js',
       'rollup*.config.js',
       'rollup*.config.js',
       'scripts/**',
       'scripts/**',
+      'benchmark/*',
       './*.{js,ts}',
       './*.{js,ts}',
       'packages/*/*.js',
       'packages/*/*.js',
       'packages/vue/*/*.js',
       'packages/vue/*/*.js',

+ 1 - 3
playground/setup/dev.js

@@ -1,10 +1,8 @@
 // @ts-check
 // @ts-check
 import path from 'node:path'
 import path from 'node:path'
-import { fileURLToPath } from 'node:url'
 
 
-const dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
 const resolve = (/** @type {string} */ p) =>
 const resolve = (/** @type {string} */ p) =>
-  path.resolve(dirname, '../../packages', p)
+  path.resolve(import.meta.dirname, '../../packages', p)
 
 
 /**
 /**
  * @param {Object} [env]
  * @param {Object} [env]

+ 0 - 60
playground/src/bench/profiling.ts

@@ -1,60 +0,0 @@
-// @ts-expect-error
-globalThis.doProfile = false
-// const defer = nextTick
-const ric =
-  typeof requestIdleCallback === 'undefined' ? setTimeout : requestIdleCallback
-export const defer = () => new Promise(r => ric(r))
-
-const times: Record<string, number[]> = {}
-
-export const wrap = (
-  id: string,
-  fn: (...args: any[]) => any,
-): ((...args: any[]) => Promise<void>) => {
-  if (import.meta.env.PROD) return fn
-  return async (...args) => {
-    const btns = Array.from(
-      document.querySelectorAll<HTMLButtonElement>('#control button'),
-    )
-    for (const node of btns) {
-      node.disabled = true
-    }
-    const doProfile = (globalThis as any).doProfile
-    await defer()
-
-    doProfile && console.profile(id)
-    const start = performance.now()
-    fn(...args)
-    await defer()
-    const time = performance.now() - start
-    const prevTimes = times[id] || (times[id] = [])
-    prevTimes.push(time)
-    const median = prevTimes.slice().sort((a, b) => a - b)[
-      Math.floor(prevTimes.length / 2)
-    ]
-    const mean = prevTimes.reduce((a, b) => a + b, 0) / prevTimes.length
-    const msg =
-      `${id}: min: ${Math.min(...prevTimes).toFixed(2)} / ` +
-      `max: ${Math.max(...prevTimes).toFixed(2)} / ` +
-      `median: ${median.toFixed(2)}ms / ` +
-      `mean: ${mean.toFixed(2)}ms / ` +
-      `time: ${time.toFixed(2)}ms / ` +
-      `std: ${getStandardDeviation(prevTimes).toFixed(2)} ` +
-      `over ${prevTimes.length} runs`
-    doProfile && console.profileEnd(id)
-    console.log(msg)
-    document.getElementById('time')!.textContent = msg
-
-    for (const node of btns) {
-      node.disabled = false
-    }
-  }
-}
-
-function getStandardDeviation(array: number[]) {
-  const n = array.length
-  const mean = array.reduce((a, b) => a + b) / n
-  return Math.sqrt(
-    array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
-  )
-}

+ 222 - 21
pnpm-lock.yaml

@@ -4,27 +4,6 @@ settings:
   autoInstallPeers: true
   autoInstallPeers: true
   excludeLinksFromLockfile: false
   excludeLinksFromLockfile: false
 
 
-catalogs:
-  default:
-    '@babel/parser':
-      specifier: ^7.24.7
-      version: 7.24.7
-    '@babel/types':
-      specifier: ^7.24.7
-      version: 7.24.7
-    estree-walker:
-      specifier: ^2.0.2
-      version: 2.0.2
-    magic-string:
-      specifier: ^0.30.10
-      version: 0.30.10
-    source-map-js:
-      specifier: ^1.2.0
-      version: 1.2.0
-    vite:
-      specifier: ^5.3.3
-      version: 5.3.3
-
 importers:
 importers:
 
 
   .:
   .:
@@ -177,6 +156,25 @@ importers:
         specifier: ^1.6.0
         specifier: ^1.6.0
         version: 1.6.0(@types/node@20.14.13)(@vitest/ui@1.6.0)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.1)
         version: 1.6.0(@types/node@20.14.13)(@vitest/ui@1.6.0)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.1)
 
 
+  benchmark:
+    dependencies:
+      '@vitejs/plugin-vue':
+        specifier: npm:@vue-vapor/vite-plugin-vue@0.0.0-alpha.6
+        version: '@vue-vapor/vite-plugin-vue@0.0.0-alpha.6(vite@5.3.3(@types/node@20.14.13)(sass@1.77.8)(terser@5.31.1))(vue@3.4.36(typescript@5.4.5))'
+      connect:
+        specifier: ^3.7.0
+        version: 3.7.0
+      sirv:
+        specifier: ^2.0.4
+        version: 2.0.4
+      vite:
+        specifier: ^5.0.12
+        version: 5.3.3(@types/node@20.14.13)(sass@1.77.8)(terser@5.31.1)
+    devDependencies:
+      '@types/connect':
+        specifier: ^3.4.38
+        version: 3.4.38
+
   packages/compiler-core:
   packages/compiler-core:
     dependencies:
     dependencies:
       '@babel/parser':
       '@babel/parser':
@@ -1418,6 +1416,9 @@ packages:
   '@tootallnate/quickjs-emscripten@0.23.0':
   '@tootallnate/quickjs-emscripten@0.23.0':
     resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
     resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
 
 
+  '@types/connect@3.4.38':
+    resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+
   '@types/estree@1.0.5':
   '@types/estree@1.0.5':
     resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
     resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
 
 
@@ -1566,13 +1567,49 @@ packages:
       vite: ^5.0.0
       vite: ^5.0.0
       vue: '*'
       vue: '*'
 
 
+  '@vue-vapor/vite-plugin-vue@0.0.0-alpha.6':
+    resolution: {integrity: sha512-V2aTQ7bkDXsoPvYIkTA54m3ypUXDIVpTFspn+ycuYcMfIY37cZ0ny6jm/afNY6k1DiaQ9JfAMBXAKzTBpu2B9A==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    peerDependencies:
+      vite: ^5.0.0
+      vue: '*'
+
+  '@vue/compiler-core@3.4.36':
+    resolution: {integrity: sha512-qBkndgpwFKdupmOPoiS10i7oFdN7a+4UNDlezD0GlQ1kuA1pNrscg9g12HnB5E8hrWSuEftRsbJhL1HI2zpJhg==}
+
+  '@vue/compiler-dom@3.4.36':
+    resolution: {integrity: sha512-eEIjy4GwwZTFon/Y+WO8tRRNGqylaRlA79T1RLhUpkOzJ7EtZkkb8MurNfkqY6x6Qiu0R7ESspEF7GkPR/4yYg==}
+
+  '@vue/compiler-sfc@3.4.36':
+    resolution: {integrity: sha512-rhuHu7qztt/rNH90dXPTzhB7hLQT2OC4s4GrPVqmzVgPY4XBlfWmcWzn4bIPEWNImt0CjO7kfHAf/1UXOtx3vw==}
+
+  '@vue/compiler-ssr@3.4.36':
+    resolution: {integrity: sha512-Wt1zyheF0zVvRJyhY74uxQbnkXV2Le/JPOrAxooR4rFYKC7cFr+cRqW6RU3cM/bsTy7sdZ83IDuy/gLPSfPGng==}
+
   '@vue/consolidate@1.0.0':
   '@vue/consolidate@1.0.0':
     resolution: {integrity: sha512-oTyUE+QHIzLw2PpV14GD/c7EohDyP64xCniWTcqcEmTd699eFqTIwOmtDYjcO1j3QgdXoJEoWv1/cCdLrRoOfg==}
     resolution: {integrity: sha512-oTyUE+QHIzLw2PpV14GD/c7EohDyP64xCniWTcqcEmTd699eFqTIwOmtDYjcO1j3QgdXoJEoWv1/cCdLrRoOfg==}
     engines: {node: '>= 0.12.0'}
     engines: {node: '>= 0.12.0'}
 
 
+  '@vue/reactivity@3.4.36':
+    resolution: {integrity: sha512-wN1aoCwSoqrt1yt8wO0gc13QaC+Vk1o6AoSt584YHNnz6TGDhh1NCMUYgAnvp4HEIkLdGsaC1bvu/P+wpoDEXw==}
+
   '@vue/repl@4.3.1':
   '@vue/repl@4.3.1':
     resolution: {integrity: sha512-yzUuLhR+MqOGBDES+xbnm27SfPIEv7XKwhFWWpQhL7HUbXj77GVu+x50Q56JhCWWKTUJzk9MOvAn7bSgdvB5og==}
     resolution: {integrity: sha512-yzUuLhR+MqOGBDES+xbnm27SfPIEv7XKwhFWWpQhL7HUbXj77GVu+x50Q56JhCWWKTUJzk9MOvAn7bSgdvB5og==}
 
 
+  '@vue/runtime-core@3.4.36':
+    resolution: {integrity: sha512-9+TR14LAVEerZWLOm/N/sG2DVYhrH2bKgFrbH/FVt/Q8Jdw4OtdcGMRC6Tx8VAo0DA1eqAqrZaX0fbOaOxxZ4A==}
+
+  '@vue/runtime-dom@3.4.36':
+    resolution: {integrity: sha512-2Qe2fKkLxgZBVvHrG0QMNLL4bsx7Ae88pyXebY2WnQYABpOnGYvA+axMbcF9QwM4yxnsv+aELbC0eiNVns7mGw==}
+
+  '@vue/server-renderer@3.4.36':
+    resolution: {integrity: sha512-2XW90Rq8+Y7S1EIsAuubZVLm0gCU8HYb5mRAruFdwfC3XSOU5/YKePz29csFzsch8hXaY5UHh7ZMddmi1XTJEA==}
+    peerDependencies:
+      vue: 3.4.36
+
+  '@vue/shared@3.4.36':
+    resolution: {integrity: sha512-fdPLStwl1sDfYuUftBaUVn2pIrVFDASYerZSrlBvVBfylObPA1gtcWJHy5Ox8jLEJ524zBibss488Q3SZtU1uA==}
+
   '@vueuse/core@10.9.0':
   '@vueuse/core@10.9.0':
     resolution: {integrity: sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==}
     resolution: {integrity: sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==}
 
 
@@ -1901,6 +1938,10 @@ packages:
   confbox@0.1.7:
   confbox@0.1.7:
     resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==}
     resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==}
 
 
+  connect@3.7.0:
+    resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
+    engines: {node: '>= 0.10.0'}
+
   constantinople@4.0.1:
   constantinople@4.0.1:
     resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
     resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
 
 
@@ -2106,6 +2147,9 @@ packages:
   eastasianwidth@0.2.0:
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
 
+  ee-first@1.1.1:
+    resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
   electron-to-chromium@1.4.818:
   electron-to-chromium@1.4.818:
     resolution: {integrity: sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==}
     resolution: {integrity: sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==}
 
 
@@ -2118,6 +2162,10 @@ packages:
   emoji-regex@9.2.2:
   emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
 
 
+  encodeurl@1.0.2:
+    resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
+    engines: {node: '>= 0.8'}
+
   end-of-stream@1.4.4:
   end-of-stream@1.4.4:
     resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
     resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
 
 
@@ -2129,6 +2177,10 @@ packages:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
     engines: {node: '>=0.12'}
     engines: {node: '>=0.12'}
 
 
+  entities@5.0.0:
+    resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==}
+    engines: {node: '>=0.12'}
+
   env-paths@2.2.1:
   env-paths@2.2.1:
     resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
     resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
     engines: {node: '>=6'}
     engines: {node: '>=6'}
@@ -2174,6 +2226,9 @@ packages:
     resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
     resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
     engines: {node: '>=6'}
     engines: {node: '>=6'}
 
 
+  escape-html@1.0.3:
+    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
   escape-string-regexp@1.0.5:
   escape-string-regexp@1.0.5:
     resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
     resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
     engines: {node: '>=0.8.0'}
     engines: {node: '>=0.8.0'}
@@ -2316,6 +2371,10 @@ packages:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
     engines: {node: '>=8'}
 
 
+  finalhandler@1.1.2:
+    resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
+    engines: {node: '>= 0.8'}
+
   find-up-simple@1.0.0:
   find-up-simple@1.0.0:
     resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
     resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
@@ -2974,6 +3033,10 @@ packages:
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
     engines: {node: '>=0.10.0'}
     engines: {node: '>=0.10.0'}
 
 
+  on-finished@2.3.0:
+    resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
+    engines: {node: '>= 0.8'}
+
   on-headers@1.0.2:
   on-headers@1.0.2:
     resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
     resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
     engines: {node: '>= 0.8'}
     engines: {node: '>= 0.8'}
@@ -3038,6 +3101,10 @@ packages:
   parse5@7.1.2:
   parse5@7.1.2:
     resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
     resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
 
 
+  parseurl@1.3.3:
+    resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+    engines: {node: '>= 0.8'}
+
   path-exists@4.0.0:
   path-exists@4.0.0:
     resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
     resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
     engines: {node: '>=8'}
     engines: {node: '>=8'}
@@ -3484,6 +3551,10 @@ packages:
   stackback@0.0.2:
   stackback@0.0.2:
     resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
     resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
 
 
+  statuses@1.5.0:
+    resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
+    engines: {node: '>= 0.6'}
+
   std-env@3.7.0:
   std-env@3.7.0:
     resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
     resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
 
 
@@ -3699,6 +3770,10 @@ packages:
     resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
     resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
     engines: {node: '>= 10.0.0'}
     engines: {node: '>= 10.0.0'}
 
 
+  unpipe@1.0.0:
+    resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+    engines: {node: '>= 0.8'}
+
   untildify@4.0.0:
   untildify@4.0.0:
     resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
     resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
     engines: {node: '>=8'}
     engines: {node: '>=8'}
@@ -3724,6 +3799,10 @@ packages:
   util-deprecate@1.0.2:
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
 
+  utils-merge@1.0.1:
+    resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
+    engines: {node: '>= 0.4.0'}
+
   validate-npm-package-license@3.0.4:
   validate-npm-package-license@3.0.4:
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
 
 
@@ -3853,6 +3932,14 @@ packages:
       '@vue/composition-api':
       '@vue/composition-api':
         optional: true
         optional: true
 
 
+  vue@3.4.36:
+    resolution: {integrity: sha512-mIFvbLgjODfx3Iy1SrxOsiPpDb8Bo3EU+87ioimOZzZTOp15IEdAels70IjBOLO3ZFlLW5AhdwY4dWbXVQKYow==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   w3c-xmlserializer@5.0.0:
   w3c-xmlserializer@5.0.0:
     resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
     resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
@@ -4644,6 +4731,10 @@ snapshots:
 
 
   '@tootallnate/quickjs-emscripten@0.23.0': {}
   '@tootallnate/quickjs-emscripten@0.23.0': {}
 
 
+  '@types/connect@3.4.38':
+    dependencies:
+      '@types/node': 20.14.13
+
   '@types/estree@1.0.5': {}
   '@types/estree@1.0.5': {}
 
 
   '@types/hash-sum@1.0.2': {}
   '@types/hash-sum@1.0.2': {}
@@ -4849,10 +4940,69 @@ snapshots:
       vite: 5.2.9(@types/node@20.14.13)(sass@1.77.8)(terser@5.31.1)
       vite: 5.2.9(@types/node@20.14.13)(sass@1.77.8)(terser@5.31.1)
       vue: link:packages/vue
       vue: link:packages/vue
 
 
+  '@vue-vapor/vite-plugin-vue@0.0.0-alpha.6(vite@5.3.3(@types/node@20.14.13)(sass@1.77.8)(terser@5.31.1))(vue@3.4.36(typescript@5.4.5))':
+    dependencies:
+      vite: 5.3.3(@types/node@20.14.13)(sass@1.77.8)(terser@5.31.1)
+      vue: 3.4.36(typescript@5.4.5)
+
+  '@vue/compiler-core@3.4.36':
+    dependencies:
+      '@babel/parser': 7.24.7
+      '@vue/shared': 3.4.36
+      entities: 5.0.0
+      estree-walker: 2.0.2
+      source-map-js: 1.2.0
+
+  '@vue/compiler-dom@3.4.36':
+    dependencies:
+      '@vue/compiler-core': 3.4.36
+      '@vue/shared': 3.4.36
+
+  '@vue/compiler-sfc@3.4.36':
+    dependencies:
+      '@babel/parser': 7.24.7
+      '@vue/compiler-core': 3.4.36
+      '@vue/compiler-dom': 3.4.36
+      '@vue/compiler-ssr': 3.4.36
+      '@vue/shared': 3.4.36
+      estree-walker: 2.0.2
+      magic-string: 0.30.10
+      postcss: 8.4.40
+      source-map-js: 1.2.0
+
+  '@vue/compiler-ssr@3.4.36':
+    dependencies:
+      '@vue/compiler-dom': 3.4.36
+      '@vue/shared': 3.4.36
+
   '@vue/consolidate@1.0.0': {}
   '@vue/consolidate@1.0.0': {}
 
 
+  '@vue/reactivity@3.4.36':
+    dependencies:
+      '@vue/shared': 3.4.36
+
   '@vue/repl@4.3.1': {}
   '@vue/repl@4.3.1': {}
 
 
+  '@vue/runtime-core@3.4.36':
+    dependencies:
+      '@vue/reactivity': 3.4.36
+      '@vue/shared': 3.4.36
+
+  '@vue/runtime-dom@3.4.36':
+    dependencies:
+      '@vue/reactivity': 3.4.36
+      '@vue/runtime-core': 3.4.36
+      '@vue/shared': 3.4.36
+      csstype: 3.1.3
+
+  '@vue/server-renderer@3.4.36(vue@3.4.36(typescript@5.4.5))':
+    dependencies:
+      '@vue/compiler-ssr': 3.4.36
+      '@vue/shared': 3.4.36
+      vue: 3.4.36(typescript@5.4.5)
+
+  '@vue/shared@3.4.36': {}
+
   '@vueuse/core@10.9.0(vue@packages+vue)':
   '@vueuse/core@10.9.0(vue@packages+vue)':
     dependencies:
     dependencies:
       '@types/web-bluetooth': 0.0.20
       '@types/web-bluetooth': 0.0.20
@@ -5201,6 +5351,15 @@ snapshots:
 
 
   confbox@0.1.7: {}
   confbox@0.1.7: {}
 
 
+  connect@3.7.0:
+    dependencies:
+      debug: 2.6.9
+      finalhandler: 1.1.2
+      parseurl: 1.3.3
+      utils-merge: 1.0.1
+    transitivePeerDependencies:
+      - supports-color
+
   constantinople@4.0.1:
   constantinople@4.0.1:
     dependencies:
     dependencies:
       '@babel/parser': 7.24.7
       '@babel/parser': 7.24.7
@@ -5394,6 +5553,8 @@ snapshots:
 
 
   eastasianwidth@0.2.0: {}
   eastasianwidth@0.2.0: {}
 
 
+  ee-first@1.1.1: {}
+
   electron-to-chromium@1.4.818: {}
   electron-to-chromium@1.4.818: {}
 
 
   emoji-regex@10.3.0: {}
   emoji-regex@10.3.0: {}
@@ -5402,6 +5563,8 @@ snapshots:
 
 
   emoji-regex@9.2.2: {}
   emoji-regex@9.2.2: {}
 
 
+  encodeurl@1.0.2: {}
+
   end-of-stream@1.4.4:
   end-of-stream@1.4.4:
     dependencies:
     dependencies:
       once: 1.4.0
       once: 1.4.0
@@ -5413,6 +5576,8 @@ snapshots:
 
 
   entities@4.5.0: {}
   entities@4.5.0: {}
 
 
+  entities@5.0.0: {}
+
   env-paths@2.2.1: {}
   env-paths@2.2.1: {}
 
 
   error-ex@1.3.2:
   error-ex@1.3.2:
@@ -5516,6 +5681,8 @@ snapshots:
 
 
   escalade@3.1.2: {}
   escalade@3.1.2: {}
 
 
+  escape-html@1.0.3: {}
+
   escape-string-regexp@1.0.5: {}
   escape-string-regexp@1.0.5: {}
 
 
   escape-string-regexp@4.0.0: {}
   escape-string-regexp@4.0.0: {}
@@ -5725,6 +5892,18 @@ snapshots:
     dependencies:
     dependencies:
       to-regex-range: 5.0.1
       to-regex-range: 5.0.1
 
 
+  finalhandler@1.1.2:
+    dependencies:
+      debug: 2.6.9
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      on-finished: 2.3.0
+      parseurl: 1.3.3
+      statuses: 1.5.0
+      unpipe: 1.0.0
+    transitivePeerDependencies:
+      - supports-color
+
   find-up-simple@1.0.0: {}
   find-up-simple@1.0.0: {}
 
 
   find-up@5.0.0:
   find-up@5.0.0:
@@ -6362,6 +6541,10 @@ snapshots:
 
 
   object-assign@4.1.1: {}
   object-assign@4.1.1: {}
 
 
+  on-finished@2.3.0:
+    dependencies:
+      ee-first: 1.1.1
+
   on-headers@1.0.2: {}
   on-headers@1.0.2: {}
 
 
   once@1.4.0:
   once@1.4.0:
@@ -6447,6 +6630,8 @@ snapshots:
     dependencies:
     dependencies:
       entities: 4.5.0
       entities: 4.5.0
 
 
+  parseurl@1.3.3: {}
+
   path-exists@4.0.0: {}
   path-exists@4.0.0: {}
 
 
   path-is-absolute@1.0.1: {}
   path-is-absolute@1.0.1: {}
@@ -6976,6 +7161,8 @@ snapshots:
 
 
   stackback@0.0.2: {}
   stackback@0.0.2: {}
 
 
+  statuses@1.5.0: {}
+
   std-env@3.7.0: {}
   std-env@3.7.0: {}
 
 
   streamx@2.18.0:
   streamx@2.18.0:
@@ -7171,6 +7358,8 @@ snapshots:
 
 
   universalify@2.0.1: {}
   universalify@2.0.1: {}
 
 
+  unpipe@1.0.0: {}
+
   untildify@4.0.0: {}
   untildify@4.0.0: {}
 
 
   update-browserslist-db@1.1.0(browserslist@4.23.1):
   update-browserslist-db@1.1.0(browserslist@4.23.1):
@@ -7197,6 +7386,8 @@ snapshots:
 
 
   util-deprecate@1.0.2: {}
   util-deprecate@1.0.2: {}
 
 
+  utils-merge@1.0.1: {}
+
   validate-npm-package-license@3.0.4:
   validate-npm-package-license@3.0.4:
     dependencies:
     dependencies:
       spdx-correct: 3.2.0
       spdx-correct: 3.2.0
@@ -7332,6 +7523,16 @@ snapshots:
     dependencies:
     dependencies:
       vue: link:packages/vue
       vue: link:packages/vue
 
 
+  vue@3.4.36(typescript@5.4.5):
+    dependencies:
+      '@vue/compiler-dom': 3.4.36
+      '@vue/compiler-sfc': 3.4.36
+      '@vue/runtime-dom': 3.4.36
+      '@vue/server-renderer': 3.4.36(vue@3.4.36(typescript@5.4.5))
+      '@vue/shared': 3.4.36
+    optionalDependencies:
+      typescript: 5.4.5
+
   w3c-xmlserializer@5.0.0:
   w3c-xmlserializer@5.0.0:
     dependencies:
     dependencies:
       xml-name-validator: 5.0.0
       xml-name-validator: 5.0.0

+ 1 - 0
pnpm-workspace.yaml

@@ -1,6 +1,7 @@
 packages:
 packages:
   - 'packages/*'
   - 'packages/*'
   - playground
   - playground
+  - benchmark
 
 
 catalog:
 catalog:
   '@babel/parser': ^7.24.7
   '@babel/parser': ^7.24.7

+ 3 - 1
scripts/inline-enums.js

@@ -198,7 +198,9 @@ export function scanEnums() {
   }
   }
 
 
   // 3. save cache
   // 3. save cache
-  if (!existsSync('temp')) mkdirSync('temp')
+  try {
+    mkdirSync('temp')
+  } catch {}
 
 
   /** @type {EnumData} */
   /** @type {EnumData} */
   const enumData = {
   const enumData = {

+ 1 - 10
scripts/release.js

@@ -6,7 +6,7 @@ import semver from 'semver'
 import enquirer from 'enquirer'
 import enquirer from 'enquirer'
 import { createRequire } from 'node:module'
 import { createRequire } from 'node:module'
 import { fileURLToPath } from 'node:url'
 import { fileURLToPath } from 'node:url'
-import { exec } from './utils.js'
+import { exec, getSha } from './utils.js'
 import { parseArgs } from 'node:util'
 import { parseArgs } from 'node:util'
 
 
 /**
 /**
@@ -445,15 +445,6 @@ async function isInSyncWithRemote() {
   }
   }
 }
 }
 
 
-/**
- * @param {boolean=} short
- */
-async function getSha(short) {
-  return (
-    await exec('git', ['rev-parse', ...(short ? ['--short'] : []), 'HEAD'])
-  ).stdout
-}
-
 async function getBranch() {
 async function getBranch() {
   return (await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout
   return (await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout
 }
 }

+ 17 - 4
scripts/utils.js

@@ -3,17 +3,20 @@ import fs from 'node:fs'
 import pico from 'picocolors'
 import pico from 'picocolors'
 import { createRequire } from 'node:module'
 import { createRequire } from 'node:module'
 import { spawn } from 'node:child_process'
 import { spawn } from 'node:child_process'
+import path from 'node:path'
 
 
 const require = createRequire(import.meta.url)
 const require = createRequire(import.meta.url)
+const packagesPath = path.resolve(import.meta.dirname, '../packages')
 
 
-export const targets = fs.readdirSync('packages').filter(f => {
+export const targets = fs.readdirSync(packagesPath).filter(f => {
+  const folder = path.resolve(packagesPath, f)
   if (
   if (
-    !fs.statSync(`packages/${f}`).isDirectory() ||
-    !fs.existsSync(`packages/${f}/package.json`)
+    !fs.statSync(folder).isDirectory() ||
+    !fs.existsSync(`${folder}/package.json`)
   ) {
   ) {
     return false
     return false
   }
   }
-  const pkg = require(`../packages/${f}/package.json`)
+  const pkg = require(`${folder}/package.json`)
   if (pkg.private && !pkg.buildOptions) {
   if (pkg.private && !pkg.buildOptions) {
     return false
     return false
   }
   }
@@ -61,6 +64,7 @@ export function fuzzyMatchTarget(partialTargets, includeAllMatching) {
  * @param {string} command
  * @param {string} command
  * @param {ReadonlyArray<string>} args
  * @param {ReadonlyArray<string>} args
  * @param {object} [options]
  * @param {object} [options]
+ * @returns {Promise<{ ok: boolean, code: number | null, stderr: string, stdout: string }>}
  */
  */
 export async function exec(command, args, options) {
 export async function exec(command, args, options) {
   return new Promise((resolve, reject) => {
   return new Promise((resolve, reject) => {
@@ -104,3 +108,12 @@ export async function exec(command, args, options) {
     })
     })
   })
   })
 }
 }
+
+/**
+ * @param {boolean=} short
+ */
+export async function getSha(short) {
+  return (
+    await exec('git', ['rev-parse', ...(short ? ['--short'] : []), 'HEAD'])
+  ).stdout
+}