From 1ee76faf55f83e20d0c2fe2d33720798bc85f967 Mon Sep 17 00:00:00 2001 From: robonen Date: Sun, 21 Jun 2026 03:14:19 +0700 Subject: [PATCH] feat: add feature plugin tests and validation for feature flags --- .gitignore | 3 +- vite-layers/README.md | 163 +- vite-layers/bin/vite-layers.mjs | 12 +- vite-layers/example/apps/aurora/app.config.ts | 10 + vite-layers/example/apps/aurora/index.html | 13 + .../example/apps/aurora/public/logo.svg | 12 + .../example/apps/aurora/src/assets/theme.css | 14 + vite-layers/example/apps/aurora/src/main.ts | 4 + .../example/apps/aurora/src/pages/Landing.vue | 67 + vite-layers/example/apps/aurora/tsconfig.json | 3 + .../example/apps/aurora/vite.config.ts | 3 + vite-layers/example/apps/brand/app.config.ts | 3 + vite-layers/example/apps/brand/index.html | 2 +- .../example/apps/brand/public/logo.svg | 12 +- .../example/apps/brand/src/assets/theme.css | 14 + .../apps/brand/src/components/AppHeader.vue | 9 - .../example/apps/brand/src/pages/Landing.vue | 80 + vite-layers/example/apps/main/app.config.ts | 27 +- vite-layers/example/apps/main/index.html | 2 +- .../example/apps/main/public/favicon.svg | 13 +- vite-layers/example/apps/main/public/logo.svg | 13 +- vite-layers/example/apps/main/src/App.vue | 19 + .../example/apps/main/src/assets/theme.css | 15 + .../apps/main/src/components/AppFooter.vue | 49 + .../apps/main/src/components/AppHeader.vue | 54 +- vite-layers/example/apps/main/src/main.ts | 33 +- .../example/apps/main/src/pages/Billing.vue | 70 +- .../example/apps/main/src/pages/Landing.vue | 84 + .../example/apps/main/src/pages/Profile.vue | 114 ++ vite-layers/example/apps/main/src/router.ts | 41 + vite-layers/example/apps/main/src/style.css | 23 + vite-layers/package.json | 45 +- vite-layers/pnpm-lock.yaml | 1753 ++++++++++++++++- vite-layers/src/config.ts | 25 +- vite-layers/src/dev.ts | 79 +- vite-layers/src/devtools.ts | 795 ++++++++ vite-layers/src/feature.ts | 43 + vite-layers/src/features.ts | 479 +++++ vite-layers/src/index.ts | 17 +- vite-layers/src/kit.ts | 94 +- vite-layers/src/public.ts | 60 +- vite-layers/src/resolve.ts | 185 +- vite-layers/src/tsconfig.ts | 60 +- vite-layers/src/types.ts | 19 +- vite-layers/src/util.ts | 8 + vite-layers/test/dev.test.ts | 38 +- vite-layers/test/devtools.test.ts | 250 +++ vite-layers/test/features.test.ts | 173 ++ .../test/fixtures/devtools/app/app.config.ts | 5 + .../fixtures/devtools/app/public/logo.svg | 1 + .../devtools/app/src/components/Header.vue | 1 + .../test/fixtures/devtools/base/app.config.ts | 7 + .../fixtures/devtools/base/public/favicon.svg | 1 + .../fixtures/devtools/base/public/logo.svg | 1 + .../devtools/base/src/components/Footer.vue | 1 + .../devtools/base/src/components/Header.vue | 1 + .../devtools/base/src/pages/Billing.vue | 1 + .../deep/base/src/components/Widget.vue | 1 + .../deep/mid/src/components/Widget.vue | 1 + .../deep/top/src/components/Widget.vue | 1 + vite-layers/test/kit.test.ts | 59 +- vite-layers/test/public.test.ts | 67 +- vite-layers/test/resolve.test.ts | 104 +- vite-layers/test/tsconfig.test.ts | 24 +- vite-layers/tsdown.config.ts | 27 + 65 files changed, 4992 insertions(+), 415 deletions(-) create mode 100644 vite-layers/example/apps/aurora/app.config.ts create mode 100644 vite-layers/example/apps/aurora/index.html create mode 100644 vite-layers/example/apps/aurora/public/logo.svg create mode 100644 vite-layers/example/apps/aurora/src/assets/theme.css create mode 100644 vite-layers/example/apps/aurora/src/main.ts create mode 100644 vite-layers/example/apps/aurora/src/pages/Landing.vue create mode 100644 vite-layers/example/apps/aurora/tsconfig.json create mode 100644 vite-layers/example/apps/aurora/vite.config.ts create mode 100644 vite-layers/example/apps/brand/src/assets/theme.css delete mode 100644 vite-layers/example/apps/brand/src/components/AppHeader.vue create mode 100644 vite-layers/example/apps/brand/src/pages/Landing.vue create mode 100644 vite-layers/example/apps/main/src/App.vue create mode 100644 vite-layers/example/apps/main/src/assets/theme.css create mode 100644 vite-layers/example/apps/main/src/components/AppFooter.vue create mode 100644 vite-layers/example/apps/main/src/pages/Landing.vue create mode 100644 vite-layers/example/apps/main/src/pages/Profile.vue create mode 100644 vite-layers/example/apps/main/src/router.ts create mode 100644 vite-layers/example/apps/main/src/style.css create mode 100644 vite-layers/src/devtools.ts create mode 100644 vite-layers/src/feature.ts create mode 100644 vite-layers/src/features.ts create mode 100644 vite-layers/src/util.ts create mode 100644 vite-layers/test/devtools.test.ts create mode 100644 vite-layers/test/features.test.ts create mode 100644 vite-layers/test/fixtures/devtools/app/app.config.ts create mode 100644 vite-layers/test/fixtures/devtools/app/public/logo.svg create mode 100644 vite-layers/test/fixtures/devtools/app/src/components/Header.vue create mode 100644 vite-layers/test/fixtures/devtools/base/app.config.ts create mode 100644 vite-layers/test/fixtures/devtools/base/public/favicon.svg create mode 100644 vite-layers/test/fixtures/devtools/base/public/logo.svg create mode 100644 vite-layers/test/fixtures/devtools/base/src/components/Footer.vue create mode 100644 vite-layers/test/fixtures/devtools/base/src/components/Header.vue create mode 100644 vite-layers/test/fixtures/devtools/base/src/pages/Billing.vue create mode 100644 vite-layers/test/fixtures/resolve/deep/base/src/components/Widget.vue create mode 100644 vite-layers/test/fixtures/resolve/deep/mid/src/components/Widget.vue create mode 100644 vite-layers/test/fixtures/resolve/deep/top/src/components/Widget.vue create mode 100644 vite-layers/tsdown.config.ts diff --git a/.gitignore b/.gitignore index 496ee2c..13430c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +.claude \ No newline at end of file diff --git a/vite-layers/README.md b/vite-layers/README.md index ae55200..488b9f2 100644 --- a/vite-layers/README.md +++ b/vite-layers/README.md @@ -45,34 +45,32 @@ export default buildViteConfig(import.meta.dirname) Гейтите опциональные страницы так, чтобы выключенные **исчезали из бандла** (а не просто переставали роутиться): ```ts -// __FEATURES__ типизируется сгенерированным .vite-layers/features.d.ts — declare не нужен +import { feature } from '#feature' // алиас регистрирует vite-layers; типы — из .vite-layers/features.d.ts + const routes = [ { path: '/', component: () => import('@/pages/Home') }, - ...(__FEATURES__.billing ? [{ path: '/billing', component: () => import('@/pages/Billing') }] : []), + ...(feature('billing') ? [{ path: '/billing', component: () => import('@/pages/Billing') }] : []), ] ``` -Тип `__FEATURES__` генерируется из `merged.features` в `.vite-layers/features.d.ts`, поэтому опечатка -(`__FEATURES__.biling`) — ошибка компиляции, а не молчком-falsy. +`feature('key')` — **компайл-тайм макрос**: плагин заменяет вызов на литерал значения флага, +**одинаково в dev и в build** (один AST-transform, без расхождений). Подставленный `false` делает ветку +статически мёртвой, и Rollup/rolldown вырезает её вместе с `import()` — чанк выключенной фичи не эмитится. +Тип `feature` генерируется из `merged.features` **литеральными типами**, поэтому опечатка ключа +(`feature('biling')`) — ошибка компиляции. -Флаги вшиваются через `define` и сворачиваются esbuild ещё **до** построения графа Rollup, поэтому -выключенная ветка и её `import()` физически не попадают в бандл. Дотированные литералы эмитятся на -любую глубину (`__FEATURES__.nested.enabled` тоже сворачивается). Правила, чтобы DCE сработало: +Правила (они **enforced**: нарушение валит сборку — и в dev, и в build, ничего не «протекает» молча): -- обращайтесь **напрямую** — `__FEATURES__.billing`; алиас/деструктуризация (`const f = __FEATURES__; f.billing`) - и динамический доступ (`__FEATURES__[name]`) не сворачиваются; -- гейт оборачивает сам `import()` (тернарник/`&&`/спред), а не `.filter` после — reachable-импорт не вырезается; -- ключи фич — валидные JS-идентификаторы (kebab/пробел доступны в рантайме через объект `__FEATURES__`, - но без DCE); -- в тестах продублируйте `define` (в `vitest.config`) или гардите `globalThis.__FEATURES__ ?? {}`. +- ключ — строковый литерал: `feature('billing')`, не `feature(name)`; +- вызывайте напрямую — без алиасов (`const f = feature`), деструктуризации и передачи как значения; +- вложенные флаги — дотированным ключом: `feature('payments.stripe')`; +- ключ должен существовать в `merged.features` (иначе — ошибка сборки). -**Dev-режим.** В build флаги работают через `define` (+ DCE). В dev Vite 8 / rolldown-vite **не** -инлайнит пользовательский `define` в исходники, поэтому `vite-layers` сам подставляет `__FEATURES__` -в рантайме (dev-only плагин). Плюс при изменении любого `app.config.*` слоя dev-сервер -**автоматически перезапускается** (`app.config` грузится c12, вне графа Vite — сам он не следит) — -так фичи обновляются без ручного рестарта. В шаблонах `.vue` `__FEATURES__` напрямую использовать -нельзя — компилятор префиксует его в `_ctx.__FEATURES__` (define/рантайм-подстановка не матчат); -читайте флаг в ` + + diff --git a/vite-layers/example/apps/aurora/public/logo.svg b/vite-layers/example/apps/aurora/public/logo.svg new file mode 100644 index 0000000..864a89d --- /dev/null +++ b/vite-layers/example/apps/aurora/public/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + Aurora + diff --git a/vite-layers/example/apps/aurora/src/assets/theme.css b/vite-layers/example/apps/aurora/src/assets/theme.css new file mode 100644 index 0000000..0673538 --- /dev/null +++ b/vite-layers/example/apps/aurora/src/assets/theme.css @@ -0,0 +1,14 @@ +/* Aurora — a dark brand. The same shared header, footer, profile and billing pages render dark + purely because every token below flips; not one component is overridden. Rose primary, sky accent. */ +:root { + --c-canvas: #0a0f1f; + --c-surface: #121829; + --c-ink: #eef1f8; + --c-muted: #9aa3bd; + --c-line: #232b42; + --c-brand: #fb7185; + --c-on-brand: #1a1022; + --c-accent: #38bdf8; + --c-radius: 1rem; + --c-font: "Inter", ui-sans-serif, system-ui, sans-serif; +} diff --git a/vite-layers/example/apps/aurora/src/main.ts b/vite-layers/example/apps/aurora/src/main.ts new file mode 100644 index 0000000..ae2c11b --- /dev/null +++ b/vite-layers/example/apps/aurora/src/main.ts @@ -0,0 +1,4 @@ +// Aurora has no bootstrap logic of its own — it reuses the base layer's entry. +// `@/main.ts` resolves to *this* file first, but the layered resolver's self-skip +// (super() semantics) falls through to the next layer, i.e. main/src/main.ts. +import '@/main.ts' diff --git a/vite-layers/example/apps/aurora/src/pages/Landing.vue b/vite-layers/example/apps/aurora/src/pages/Landing.vue new file mode 100644 index 0000000..d0ab22d --- /dev/null +++ b/vite-layers/example/apps/aurora/src/pages/Landing.vue @@ -0,0 +1,67 @@ + + + diff --git a/vite-layers/example/apps/aurora/tsconfig.json b/vite-layers/example/apps/aurora/tsconfig.json new file mode 100644 index 0000000..2955843 --- /dev/null +++ b/vite-layers/example/apps/aurora/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.vite-layers/tsconfig.json" +} diff --git a/vite-layers/example/apps/aurora/vite.config.ts b/vite-layers/example/apps/aurora/vite.config.ts new file mode 100644 index 0000000..6656ad6 --- /dev/null +++ b/vite-layers/example/apps/aurora/vite.config.ts @@ -0,0 +1,3 @@ +import { buildViteConfig } from '../../../src/index.ts' + +export default buildViteConfig(import.meta.dirname) diff --git a/vite-layers/example/apps/brand/app.config.ts b/vite-layers/example/apps/brand/app.config.ts index 12f1400..0153e9f 100644 --- a/vite-layers/example/apps/brand/app.config.ts +++ b/vite-layers/example/apps/brand/app.config.ts @@ -1,5 +1,8 @@ import { defineLayerConfig } from '../../../src/index.ts' +// Northwind — a reseller brand. It inherits the entire shell (header, footer, profile, router, +// Tailwind setup) from `main` and changes only three things: its logo, its theme.css tokens and +// its Landing page. It also drops the billing page entirely. export default defineLayerConfig({ name: 'brand', extends: ['../main'], diff --git a/vite-layers/example/apps/brand/index.html b/vite-layers/example/apps/brand/index.html index a793905..b0e2f80 100644 --- a/vite-layers/example/apps/brand/index.html +++ b/vite-layers/example/apps/brand/index.html @@ -4,7 +4,7 @@ - vite-layers — brand + Northwind — Cloud Platform
diff --git a/vite-layers/example/apps/brand/public/logo.svg b/vite-layers/example/apps/brand/public/logo.svg index c333118..c020abd 100644 --- a/vite-layers/example/apps/brand/public/logo.svg +++ b/vite-layers/example/apps/brand/public/logo.svg @@ -1 +1,11 @@ -BRAND_LOGO_OVERRIDE \ No newline at end of file + + + + + + + + + + Northwind + \ No newline at end of file diff --git a/vite-layers/example/apps/brand/src/assets/theme.css b/vite-layers/example/apps/brand/src/assets/theme.css new file mode 100644 index 0000000..8a649bb --- /dev/null +++ b/vite-layers/example/apps/brand/src/assets/theme.css @@ -0,0 +1,14 @@ +/* Northwind — a reseller brand. Light, emerald→sky, with rounder corners. This file is the only + styling difference from Acme: it shadows main/src/assets/theme.css through the layer resolver. */ +:root { + --c-canvas: #f6fbf8; + --c-surface: #ffffff; + --c-ink: #0b231b; + --c-muted: #5b7a6e; + --c-line: #dcebe3; + --c-brand: #10b981; + --c-on-brand: #ffffff; + --c-accent: #0ea5e9; + --c-radius: 1.25rem; + --c-font: "Inter", ui-sans-serif, system-ui, sans-serif; +} diff --git a/vite-layers/example/apps/brand/src/components/AppHeader.vue b/vite-layers/example/apps/brand/src/components/AppHeader.vue deleted file mode 100644 index 4812ae8..0000000 --- a/vite-layers/example/apps/brand/src/components/AppHeader.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/vite-layers/example/apps/brand/src/pages/Landing.vue b/vite-layers/example/apps/brand/src/pages/Landing.vue new file mode 100644 index 0000000..6d52fc8 --- /dev/null +++ b/vite-layers/example/apps/brand/src/pages/Landing.vue @@ -0,0 +1,80 @@ + + + diff --git a/vite-layers/example/apps/main/app.config.ts b/vite-layers/example/apps/main/app.config.ts index f28d320..8757fab 100644 --- a/vite-layers/example/apps/main/app.config.ts +++ b/vite-layers/example/apps/main/app.config.ts @@ -1,18 +1,29 @@ import vue from '@vitejs/plugin-vue' +import tailwind from '@tailwindcss/vite' +import { DevTools } from '@vitejs/devtools' import { defineLayerConfig } from '../../../src/index.ts' export default defineLayerConfig({ name: 'main', - features: { billing: true, p2p: true }, - // The framework plugin lives in the layer config, not in vite-layers' core. - vite: { - plugins: [vue()], + // `billing` gates a whole page (build-time DCE); `betaBanner` toggles a UI accent and is + // turned off in production via the `$production` env override below. + features: { billing: true, betaBanner: true }, + + // Framework + CSS plugins live in the layer config, not in vite-layers' core. Brands inherit them. + // The Vite DevTools hub is added in dev only (`DevTools()` is async → a Promise, a valid + // plugin entry); vite-layers auto-mounts its Layers / Features / Resolver / Public & TS panels into + // it via `buildViteConfig`. Build output is untouched (the hub is excluded when command is 'build'). + vite: ({ command }) => ({ + plugins: [vue(), tailwind(), command === 'serve' && DevTools()], build: { rolldownOptions: { input: { main: '@/main.ts' } } }, - }, + }), + // Per-layer tsconfig tweaks (merged across the stack, like Nuxt's typescript.tsConfig). - tsConfig: { compilerOptions: { jsx: 'preserve', jsxImportSource: 'vue' } }, + // `vite/client` supplies ambient module decls for the CSS side-effect imports (`*.css`) and + // `import.meta.env`. It flows into every brand's generated app tsconfig too. + tsConfig: { compilerOptions: { jsx: 'preserve', jsxImportSource: 'vue', types: ['vite/client'] } }, $production: { - features: { p2p: false }, - } + features: { betaBanner: false }, // the beta accent never ships to prod + }, }) diff --git a/vite-layers/example/apps/main/index.html b/vite-layers/example/apps/main/index.html index aa6cb24..d39151a 100644 --- a/vite-layers/example/apps/main/index.html +++ b/vite-layers/example/apps/main/index.html @@ -4,7 +4,7 @@ - vite-layers — main + Acme — Cloud Platform
diff --git a/vite-layers/example/apps/main/public/favicon.svg b/vite-layers/example/apps/main/public/favicon.svg index 4153d79..6aa78dc 100644 --- a/vite-layers/example/apps/main/public/favicon.svg +++ b/vite-layers/example/apps/main/public/favicon.svg @@ -1 +1,12 @@ -SHARED_FAVICON \ No newline at end of file + + + + + + + + + + + + \ No newline at end of file diff --git a/vite-layers/example/apps/main/public/logo.svg b/vite-layers/example/apps/main/public/logo.svg index a439ebb..f7bc26e 100644 --- a/vite-layers/example/apps/main/public/logo.svg +++ b/vite-layers/example/apps/main/public/logo.svg @@ -1 +1,12 @@ -MAIN_LOGO_SVG \ No newline at end of file + + + + + + + + + + + Acme + \ No newline at end of file diff --git a/vite-layers/example/apps/main/src/App.vue b/vite-layers/example/apps/main/src/App.vue new file mode 100644 index 0000000..5710947 --- /dev/null +++ b/vite-layers/example/apps/main/src/App.vue @@ -0,0 +1,19 @@ + + + diff --git a/vite-layers/example/apps/main/src/assets/theme.css b/vite-layers/example/apps/main/src/assets/theme.css new file mode 100644 index 0000000..ecfea7c --- /dev/null +++ b/vite-layers/example/apps/main/src/assets/theme.css @@ -0,0 +1,15 @@ +/* Acme — the base brand. Light, indigo→violet. Every other brand ships its own copy of this + file at the same path; the layer resolver picks the highest-priority one, recoloring the + shared header, footer, profile and billing pages without touching a single component. */ +:root { + --c-canvas: #fbfbfd; + --c-surface: #ffffff; + --c-ink: #18181b; + --c-muted: #6b7280; + --c-line: #ececf1; + --c-brand: #6366f1; + --c-on-brand: #ffffff; + --c-accent: #a855f7; + --c-radius: 0.875rem; + --c-font: "Inter", ui-sans-serif, system-ui, sans-serif; +} diff --git a/vite-layers/example/apps/main/src/components/AppFooter.vue b/vite-layers/example/apps/main/src/components/AppFooter.vue new file mode 100644 index 0000000..3eb84d0 --- /dev/null +++ b/vite-layers/example/apps/main/src/components/AppFooter.vue @@ -0,0 +1,49 @@ + + + diff --git a/vite-layers/example/apps/main/src/components/AppHeader.vue b/vite-layers/example/apps/main/src/components/AppHeader.vue index 523dfcd..372a600 100644 --- a/vite-layers/example/apps/main/src/components/AppHeader.vue +++ b/vite-layers/example/apps/main/src/components/AppHeader.vue @@ -1,7 +1,57 @@ diff --git a/vite-layers/example/apps/main/src/main.ts b/vite-layers/example/apps/main/src/main.ts index 5eaa2b7..5d95986 100644 --- a/vite-layers/example/apps/main/src/main.ts +++ b/vite-layers/example/apps/main/src/main.ts @@ -1,27 +1,10 @@ -import { createApp, defineAsyncComponent, h, shallowRef, type Component } from 'vue' -const AppHeader = defineAsyncComponent(() => import('@/components/AppHeader.vue')) +import { createApp } from 'vue' +import App from '@/App.vue' -// `__FEATURES__` is typed by the generated `.vite-layers/features.d.ts` — no manual `declare` needed. +// `@/style.css` (Tailwind entry) lives only in the base layer, so every brand shares it. +// `@/theme.css` is layer-resolved: each brand ships its own token file that shadows the base's, +// recoloring the whole shared UI. Imported after style.css so its :root vars win the cascade. +import '@/style.css' +import '@/assets/theme.css' -// Pages are gated on build-time feature flags: a disabled page's dynamic import() is -// statically dead, so its chunk is never emitted (per-brand dead-code elimination). -const routes = [ - { path: '/', component: () => import('@/pages/Home.vue') }, - ...(__FEATURES__.billing - ? [{ path: '/billing', component: () => import('@/pages/Billing.vue') }] - : []), -] - -// A tiny hash router so `routes` (and thus the gated import) is actually reachable. -const current = shallowRef(null) -async function navigate() { - const path = location.hash.slice(1) || '/' - const route = routes.find(r => r.path === path) ?? routes[0] - current.value = route ? ((await route.component()).default as Component) : null -} -window.addEventListener('hashchange', navigate) -void navigate() - -createApp({ - render: () => h('div', [h(AppHeader), current.value ? h(current.value) : null]), -}).mount('#app') +createApp(App).mount('#app') diff --git a/vite-layers/example/apps/main/src/pages/Billing.vue b/vite-layers/example/apps/main/src/pages/Billing.vue index 04a7fea..88be976 100644 --- a/vite-layers/example/apps/main/src/pages/Billing.vue +++ b/vite-layers/example/apps/main/src/pages/Billing.vue @@ -1,3 +1,71 @@ + + diff --git a/vite-layers/example/apps/main/src/pages/Landing.vue b/vite-layers/example/apps/main/src/pages/Landing.vue new file mode 100644 index 0000000..df6f42c --- /dev/null +++ b/vite-layers/example/apps/main/src/pages/Landing.vue @@ -0,0 +1,84 @@ + + + diff --git a/vite-layers/example/apps/main/src/pages/Profile.vue b/vite-layers/example/apps/main/src/pages/Profile.vue new file mode 100644 index 0000000..6645a05 --- /dev/null +++ b/vite-layers/example/apps/main/src/pages/Profile.vue @@ -0,0 +1,114 @@ + + + diff --git a/vite-layers/example/apps/main/src/router.ts b/vite-layers/example/apps/main/src/router.ts new file mode 100644 index 0000000..d31cab6 --- /dev/null +++ b/vite-layers/example/apps/main/src/router.ts @@ -0,0 +1,41 @@ +import { markRaw, shallowRef, type Component } from 'vue' +import { feature } from '#feature' + +export interface AppRoute { + path: string + label: string + component: () => Promise<{ default: Component }> +} + +// Pages gated on build-time feature flags via the `feature()` macro (typed by the generated +// `.vite-layers/features.d.ts`). A disabled page's dynamic import() is statically dead — its branch +// folds away and the chunk is never emitted (per-brand dead-code elimination), in dev and build alike. +export const routes: AppRoute[] = [ + { path: '/', label: 'Overview', component: () => import('@/pages/Landing.vue') }, + { path: '/profile', label: 'Profile', component: () => import('@/pages/Profile.vue') }, + ...(feature('billing') + ? [{ path: '/billing', label: 'Billing', component: () => import('@/pages/Billing.vue') }] + : []), +] + +const current = shallowRef(null) +const currentPath = shallowRef('/') + +async function navigate() { + const path = location.hash.slice(1) || '/' + const route = routes.find(r => r.path === path) ?? routes[0]! + currentPath.value = route.path + current.value = markRaw((await route.component()).default) +} + +let started = false + +/** Minimal hash router shared by every brand — keeps the demo dependency-free. */ +export function useRoute() { + if (!started) { + started = true + window.addEventListener('hashchange', navigate) + void navigate() + } + return { current, currentPath } +} diff --git a/vite-layers/example/apps/main/src/style.css b/vite-layers/example/apps/main/src/style.css new file mode 100644 index 0000000..944f4c8 --- /dev/null +++ b/vite-layers/example/apps/main/src/style.css @@ -0,0 +1,23 @@ +@import "tailwindcss" source(none); + +/* Scan every layer's source for class names. `@source` is relative to this file, which always + resolves to apps/main/src/style.css (it lives only in the base layer), so `../../` is the + apps/ directory — i.e. main + every brand. CWD-independent, unlike Tailwind's auto-detection. */ +@source "../../"; + +/* Map Tailwind's design tokens onto runtime CSS variables. `inline` makes each utility reference + the variable directly — e.g. `bg-brand` becomes `background-color: var(--c-brand)` — so a brand's + theme.css (loaded after this file through the layer resolver) restyles the entire shared UI with + no change to a single component. The --c-* values are defined per brand in `@/theme.css`. */ +@theme inline { + --color-canvas: var(--c-canvas); + --color-surface: var(--c-surface); + --color-ink: var(--c-ink); + --color-muted: var(--c-muted); + --color-line: var(--c-line); + --color-brand: var(--c-brand); + --color-on-brand: var(--c-on-brand); + --color-accent: var(--c-accent); + --radius-card: var(--c-radius); + --font-sans: var(--c-font); +} diff --git a/vite-layers/package.json b/vite-layers/package.json index fc4649e..6a63093 100644 --- a/vite-layers/package.json +++ b/vite-layers/package.json @@ -8,35 +8,74 @@ "node": ">=24.0.0" }, "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./feature": "./src/feature.ts", + "./devtools": "./src/devtools.ts" }, "bin": { "vite-layers": "./bin/vite-layers.mjs" }, + "files": [ + "dist", + "bin" + ], + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./feature": { + "types": "./dist/feature.d.ts", + "import": "./dist/feature.js" + }, + "./devtools": { + "types": "./dist/devtools.d.ts", + "import": "./dist/devtools.js" + } + } + }, "scripts": { "test": "vitest run", "test:watch": "vitest", "type-check": "tsc --noEmit", - "example:build": "vite build example/apps/main && vite build example/apps/brand", - "example:check": "node bin/vite-layers.mjs prepare example/apps/main && node bin/vite-layers.mjs prepare example/apps/brand && vue-tsc --noEmit -p example/apps/main && vue-tsc --noEmit -p example/apps/main/.vite-layers/tsconfig.node.json && vue-tsc --noEmit -p example/apps/brand && vue-tsc --noEmit -p example/apps/brand/.vite-layers/tsconfig.node.json" + "build": "tsdown", + "prepack": "tsdown", + "example:build": "vite build example/apps/main && vite build example/apps/brand && vite build example/apps/aurora", + "example:main": "vite example/apps/main", + "example:brand": "vite example/apps/brand", + "example:aurora": "vite example/apps/aurora", + "example:check": "node bin/vite-layers.mjs prepare example/apps/main && node bin/vite-layers.mjs prepare example/apps/brand && node bin/vite-layers.mjs prepare example/apps/aurora && vue-tsc --noEmit -p example/apps/main && vue-tsc --noEmit -p example/apps/main/.vite-layers/tsconfig.node.json && vue-tsc --noEmit -p example/apps/brand && vue-tsc --noEmit -p example/apps/brand/.vite-layers/tsconfig.node.json && vue-tsc --noEmit -p example/apps/aurora && vue-tsc --noEmit -p example/apps/aurora/.vite-layers/tsconfig.node.json" }, "peerDependencies": { + "@vitejs/devtools-kit": "^0.3.0", "vite": "^8.0.0" }, + "peerDependenciesMeta": { + "@vitejs/devtools-kit": { + "optional": true + } + }, "dependencies": { "c12": "^3.3.4", "defu": "^6.1.4", "hookable": "^6.1.1", "jiti": "^2.4.0", "magic-string": "^0.30.21", + "oxc-parser": "^0.137.0", "pkg-types": "^2.3.1", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "ufo": "^1.6.1" }, "devDependencies": { + "@tailwindcss/vite": "^4.3.1", "@types/node": "^25.9.1", + "@vitejs/devtools": "0.3.3", + "@vitejs/devtools-kit": "0.3.3", "@vitejs/plugin-vue": "^6.0.7", + "tailwindcss": "^4.3.1", + "tsdown": "^0.22.3", "typescript": "~6.0.3", "vite": "^8.0.14", "vitest": "^4.1.7", diff --git a/vite-layers/pnpm-lock.yaml b/vite-layers/pnpm-lock.yaml index cb6f012..b6348c1 100644 --- a/vite-layers/pnpm-lock.yaml +++ b/vite-layers/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: magic-string: specifier: ^0.30.21 version: 0.30.21 + oxc-parser: + specifier: ^0.137.0 + version: 0.137.0 pkg-types: specifier: ^2.3.1 version: 2.3.1 @@ -36,21 +39,36 @@ importers: specifier: ^1.6.1 version: 1.6.4 devDependencies: + '@tailwindcss/vite': + specifier: ^4.3.1 + version: 4.3.1(vite@8.0.16) '@types/node': specifier: ^25.9.1 version: 25.9.1 + '@vitejs/devtools': + specifier: 0.3.3 + version: 0.3.3(typescript@6.0.3)(vite@8.0.16) + '@vitejs/devtools-kit': + specifier: 0.3.3 + version: 0.3.3(typescript@6.0.3)(vite@8.0.16) '@vitejs/plugin-vue': specifier: ^6.0.7 - version: 6.0.7(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3)) + version: 6.0.7(vite@8.0.16)(vue@3.5.35(typescript@6.0.3)) + tailwindcss: + specifier: ^4.3.1 + version: 4.3.1 + tsdown: + specifier: ^0.22.3 + version: 0.22.3(@vitejs/devtools@0.3.3)(publint@0.3.21)(typescript@6.0.3)(vue-tsc@3.3.3(typescript@6.0.3)) typescript: specifier: ~6.0.3 version: 6.0.3 vite: specifier: ^8.0.14 - version: 8.0.16(@types/node@25.9.1)(jiti@2.7.0) + version: 8.0.16(@types/node@25.9.1)(@vitejs/devtools@0.3.3)(jiti@2.7.0) vitest: specifier: ^4.1.7 - version: 4.1.8(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0)) + version: 4.1.8(@types/node@25.9.1)(vite@8.0.16) vue: specifier: ^3.5.35 version: 3.5.35(typescript@6.0.3) @@ -60,77 +78,437 @@ importers: packages: + '@babel/generator@8.0.0': + resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0': + resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.2': + resolution: {integrity: sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@8.0.0': + resolution: {integrity: sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@babel/types@8.0.0': + resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@devframes/hub@0.5.4': + resolution: {integrity: sha512-NZs5RFuNb6tjQd2JBsRYQT+OqE+z16Kg9/72HG4k+uJdzK90sAGtAjOwK8bsvav/9l85KGx4agfkgxnfbl313w==} + peerDependencies: + devframe: 0.5.4 + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-parser/binding-android-arm-eabi@0.132.0': + resolution: {integrity: sha512-KrLaPWa5c9Y7LkW+rKkaUE3y7DBDrQtaf7rlsSDfv6KAHUjgzAIRA761Lrrp6//Yd/Rlie/yEOt9YENCoJnOcw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm-eabi@0.137.0': + resolution: {integrity: sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.132.0': + resolution: {integrity: sha512-SThDrSeamB/kG2+NxcJ5/wSLcV6dUqDknrPLqFYQ0ST/55mtBP4M7Q/f3QbubH6aAd11wpzZn/nwbVRSdobOpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-android-arm64@0.137.0': + resolution: {integrity: sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.132.0': + resolution: {integrity: sha512-Lc0f/TYoKBghE5/2Gsv7bLXk+TJZunx2Tf61X8hG4ARXdc8UYI26dCGccFSd1AyFbK3jfaNXtMnupggDbjPXdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-arm64@0.137.0': + resolution: {integrity: sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.132.0': + resolution: {integrity: sha512-RG2eJIpf7C21z9HSSXFw1bTArdpKe7Y4fwcJTwRq1yCSe1vSavaN9GA1sm9KqzemTLAGVktQ+7qBTGp0vQeUZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.137.0': + resolution: {integrity: sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.132.0': + resolution: {integrity: sha512-wQIPntPLtJ8NcBpvKPbEv3NqzV6k8eP8tP/jE9Rg8HTg/j7urZGFSsTCPCW5k77Qfw2DM4vRvc9p3I4yq/Shvw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-freebsd-x64@0.137.0': + resolution: {integrity: sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.132.0': + resolution: {integrity: sha512-PixKEpeSe3yxQWqNyOCBALRYc72+Tj7ILDofUl3iXo25cVOzLA6jHUhmOINRtWIPh7dbUie3QNeabwaQpZTw6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + resolution: {integrity: sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.132.0': + resolution: {integrity: sha512-sCR+DzGHlyHKnbA2z9zWjTUhIo8Sy0enJl4RDsBwPmkxYynPatpwOAWe8W5127SlW0boqUWHGtr1NWn5UwIhXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + resolution: {integrity: sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.132.0': + resolution: {integrity: sha512-sQBix5P2cW+IpzTcCwYxnh9yALrKSIkKJThspBvMGcygSMnbzkSvhN7SfuX1hvBk8y1XEChsdkU3ET0V5DmzUw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + resolution: {integrity: sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.132.0': + resolution: {integrity: sha512-WozHg3Kc//8Sk756HXXgMbEAvqtG+Lzb9JOojwQzIGDtN78Az2dLttkb71akWYUF/8IgYfDSlfKh4Uot8is5Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + resolution: {integrity: sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.132.0': + resolution: {integrity: sha512-CmX/ulNBOEwWTyVRmcpYKAcAizW6+OjtLJgo7fXoL9OqQvjF4VER8tPomv44vwzfSCy1BHbsB0ZlZYzYJNj4cA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + resolution: {integrity: sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.132.0': + resolution: {integrity: sha512-j9oQS+hM90SdhviNGWbPgT4+Rlq+ac++q/zjgwPD1mVHgxHzATvoRGtDx0sXGmFOQ9J9YkwAhYGb5MAHL6TAsA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + resolution: {integrity: sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.132.0': + resolution: {integrity: sha512-bLz+Xi+Agnfmd7kWPEsSVwCn2k4EyIalZkNBcQ0OGIv9rqn8VgCPLNd03tM9mKX/5TdlvDXalz0q71BIrOPNqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + resolution: {integrity: sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.132.0': + resolution: {integrity: sha512-U6t2qbJU0ypTfyj9QV3W1Y6mITDTL8ai/OR6NUn85vyHthOvobKWgXzU4tu0EskSzlpuVFz1g0jFGulDIUKHxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + resolution: {integrity: sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.132.0': + resolution: {integrity: sha512-WcEaSNHFk8yz5YFlQQAlhq6jOFmZBB/RKE7uzhyCIf+pF1Lmv9gUH4221mle2Gd9iHyWT3ySNph8yZgb1xYdWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + resolution: {integrity: sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.132.0': + resolution: {integrity: sha512-iQrV4iJzQgRwK3BWRmQl1C3C6g3wYpXN2WLdQdyR+efoUnncdShZAVp9OgcojtlD3MDRbuOMGG3SjxF4fL4nlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + resolution: {integrity: sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.132.0': + resolution: {integrity: sha512-FWzmUGrZ6GUby4U7WIwcCtab6tdmlTO3xTRRKyb5kjIJVEiaUAT8animUG/nK8ZCA8gkRkPOTId4rl6uTqUmJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + resolution: {integrity: sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.132.0': + resolution: {integrity: sha512-TlbMppxJI5CjWDes0QaP6G3aneVg1yikBu5QYI+DUShF9WDL66ccgKFNNGmi/Wybtszw6hxwAvv76T4DaPKnHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + resolution: {integrity: sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.132.0': + resolution: {integrity: sha512-RH/NbFjGKqdUAUi7Oh3LQPxUk2hsWFEEQ38HSnbRQT8QjBZFKqL1fMbmsB3N4jy/KPh9iX94+9dmkEMBBbambw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + resolution: {integrity: sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.132.0': + resolution: {integrity: sha512-JUr4jQY9jxoIB/YTLXr6XofSi5xikj6p5/Ns1h0VOBDT0j1jKU+kMsv2xxv51RwnETcXpA1Yw/9oUAfcqfaqEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + resolution: {integrity: sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.132.0': + resolution: {integrity: sha512-2dapgHpA5X8DSXF4AU36hJWYf6zP0tKjMXFRAZFBD62pkevW/uhFDXoFH9Y/3Fd2EtDrw5ByNnR1wVE9X9y0SQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + resolution: {integrity: sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@publint/pack@0.1.5': + resolution: {integrity: sha512-edgyN2pP07uXiP4tJs0s8KVmU8M8i60YPbbI0/WDeok1mIJHRXz+CgD8I0nelwDkoCh3EWL/G5kGfbuHjsdbvw==} + engines: {node: '>=18'} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@rolldown/binding-android-arm64@1.0.3': resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.1.2': + resolution: {integrity: sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.3': resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.1.2': + resolution: {integrity: sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.3': resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.1.2': + resolution: {integrity: sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.3': resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.1.2': + resolution: {integrity: sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.1.2': + resolution: {integrity: sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.3': resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -138,6 +516,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-arm64-gnu@1.1.2': + resolution: {integrity: sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-arm64-musl@1.0.3': resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -145,6 +530,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-arm64-musl@1.1.2': + resolution: {integrity: sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-ppc64-gnu@1.0.3': resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -152,6 +544,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-ppc64-gnu@1.1.2': + resolution: {integrity: sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.3': resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -159,6 +558,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.1.2': + resolution: {integrity: sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.3': resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -166,6 +572,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.1.2': + resolution: {integrity: sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-musl@1.0.3': resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} @@ -173,35 +586,162 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-x64-musl@1.1.2': + resolution: {integrity: sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@rolldown/binding-openharmony-arm64@1.0.3': resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.1.2': + resolution: {integrity: sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.3': resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.1.2': + resolution: {integrity: sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.3': resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.1.2': + resolution: {integrity: sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.3': resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.1.2': + resolution: {integrity: sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/debug@1.1.2': + resolution: {integrity: sha512-eLvb7Vs0mUYr+lm7D3R99E4VRgJa3ABskr9pMqSBgp1kmQe+IvjNPs1QtLG9Zy2lwdLN72jn8T6SSYVuWJu7Ag==} + '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.3.1': + resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} + + '@tailwindcss/oxide-android-arm64@4.3.1': + resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.1': + resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.1': + resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.1': + resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.1': + resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.1': + resolution: {integrity: sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} @@ -214,9 +754,31 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/node@25.9.1': resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@valibot/to-json-schema@1.7.1': + resolution: {integrity: sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==} + peerDependencies: + valibot: ^1.4.0 + + '@vitejs/devtools-kit@0.3.3': + resolution: {integrity: sha512-noXyK0szYxh8ALsA7PIoFANOWx13K9NoMKqk3J1pCwGMdnhxWgSqtbhki+/REoK4itbxIFUfc0EwqsoZIKiqyg==} + peerDependencies: + vite: '*' + + '@vitejs/devtools-rolldown@0.3.3': + resolution: {integrity: sha512-J75uHwwpNtD+cQHywwZyqEURck+GbqIFYW629A07OeebFigKLUfMnOmHS1AC41Ws0XQlXkcM2ZDejSZrw+YXVg==} + + '@vitejs/devtools@0.3.3': + resolution: {integrity: sha512-jE7Bn+v1Qt3BHWl8Ir9bOSNC0+kJWStX+EK1F3D8vx9FphctEijUHqbNxGtUJSfF20btijrNvCQeKK6mQzMcaA==} + hasBin: true + peerDependencies: + vite: '*' + '@vitejs/plugin-vue@6.0.7': resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -294,13 +856,33 @@ packages: '@vue/shared@3.5.35': resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + alien-signals@3.2.1: resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-kit@3.0.0: + resolution: {integrity: sha512-8OG92q3R35qjC/4i6BLBMg8IB+fClWu/1PEwg2Z9Rn+BuNaiEgJzpzn+pxWOdHJWDCAwu2JP0wCDTozAM4QirQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -309,6 +891,10 @@ packages: magicast: optional: true + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -317,15 +903,32 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -336,10 +939,39 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devframe@0.5.4: + resolution: {integrity: sha512-dbHU/LuptR1aMXcizjHUeY3gu7qVaRQoFYqbbyyGuO6Y+avFA4uQtwinHtG1qa7lRel/7b8JrBDm0nbnxU3vqg==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.0.0 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} + engines: {node: '>=10.13.0'} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -374,17 +1006,52 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + + get-tsconfig@5.0.0-beta.5: + resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} + engines: {node: '>=20.20.0'} + giget@3.2.0: resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} hasBin: true + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + h3@2.0.1-rc.22: + resolution: {integrity: sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.1 + peerDependenciesMeta: + crossws: + optional: true + hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + import-without-cache@0.4.0: + resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} + engines: {node: ^22.18.0 || >=24.0.0} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -459,9 +1126,20 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -474,12 +1152,50 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nostics@0.2.0: + resolution: {integrity: sha512-/WQpI46UMbqvy1okYb+V+9wW3J8/m6GJ33wm691n/tyi6YtJiZ6ssJjENAU7y4evfYrrgYN9HllKDzPvffil1w==} + + nostics@0.3.0: + resolution: {integrity: sha512-tP0hvxK4n2hZO9kK5Az7dLXIFukANDpcdtMae3tvtyRQun48gZIBVyXCJc8zk+qsdOpY7ngashH0ivQGOGzmeg==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + oxc-parser@0.132.0: + resolution: {integrity: sha512-+0LAPHaqtfQlvWdpaAa09SmOaZZgP8C552xosEkGJ4+ruEwP1Vgx+sqBgcBCNfR6KDCmagGOZTde8wmAvcI/Hg==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-parser@0.137.0: + resolution: {integrity: sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==} + engines: {node: ^20.19.0 || >=22.12.0} + + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} + engines: {node: '>=20'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -492,10 +1208,17 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} @@ -503,6 +1226,17 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + publint@0.3.21: + resolution: {integrity: sha512-OqejcnMV6E9zel2oCrUOJEiiFkGiAAni0A6ibfQNh1k9Gu5z4F+Yso8lllam7AzmV6Do0vp7u3UpZNRBwuXaHQ==} + engines: {node: '>=18'} + hasBin: true + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + rc9@3.0.1: resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} @@ -510,11 +1244,50 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown-plugin-dts@0.26.0: + resolution: {integrity: sha512-e+kEPtUiDES0htk5iqkSeF4EzAV7R+vugGB44iPDuw1Kw9E+WyL1VG7PaV0IIjGHLiacztMBcMTyrr8ON9CT1Q==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0 + typescript: ^5.0.0 || ^6.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + rolldown@1.0.3: resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.1.2: + resolution: {integrity: sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rou3@0.8.1: + resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -526,12 +1299,24 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + srvx@0.11.17: + resolution: {integrity: sha512-43yM4luKfCJamyCMhrUeHUPOrf8TdZe7kN8s5zayZCH5OeprYqi49Aso5ZvHXR4aB+DHaRNO/diNFgZSMNG8Xw==} + engines: {node: '>=20.16.0'} + hasBin: true + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + tailwindcss@4.3.1: + resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -551,6 +1336,44 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tsdown@0.22.3: + resolution: {integrity: sha512-louqbfA8Qf//B9jTTL0FPtXTNpjCWv1VPkbcmQMph2pTpzs+LnB1tbe4tDDRVpo2BjF5SgUXaTZe45SxB8pWHg==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.3 + '@tsdown/exe': 0.22.3 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -562,9 +1385,92 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + unstorage@1.17.5: + resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + valibot@1.4.1: + resolution: {integrity: sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -658,6 +1564,11 @@ packages: peerDependencies: typescript: '>=5.0.0' + vue-virtual-scroller@3.0.4: + resolution: {integrity: sha512-3qh3c9VUVysuXynaa4fVZ3ncx3VgD7EPRiQcj+jUVZl5u/TTkD3c27XvSEu3JGJfsJt/vVTVziZ3djiiHtW4cQ==} + peerDependencies: + vue: ^3.3.0 + vue@3.5.35: resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==} peerDependencies: @@ -666,44 +1577,138 @@ packages: typescript: optional: true + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + snapshots: + '@babel/generator@8.0.0': + dependencies: + '@babel/parser': 8.0.0 + '@babel/types': 8.0.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-string-parser@8.0.0': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-identifier@8.0.2': {} + '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 + '@babel/parser@8.0.0': + dependencies: + '@babel/types': 8.0.0 + '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@8.0.0': + dependencies: + '@babel/helper-string-parser': 8.0.0 + '@babel/helper-validator-identifier': 8.0.2 + + '@devframes/hub@0.5.4(devframe@0.5.4(typescript@6.0.3))': + dependencies: + birpc: 4.0.0 + devframe: 0.5.4(typescript@6.0.3) + nostics: 0.2.0 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + tinyexec: 1.2.4 + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -711,46 +1716,236 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.132.0': + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.137.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.132.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.132.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.132.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.137.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.132.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.132.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.132.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.132.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.132.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.132.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.132.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.132.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.132.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.132.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.132.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.132.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.132.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.132.0': + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.132.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.132.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + optional: true + + '@oxc-project/types@0.132.0': {} + '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.137.0': {} + '@polka/url@1.0.0-next.29': {} + '@publint/pack@0.1.5': + dependencies: + tinyexec: 1.2.4 + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + '@rolldown/binding-android-arm64@1.0.3': optional: true + '@rolldown/binding-android-arm64@1.1.2': + optional: true + '@rolldown/binding-darwin-arm64@1.0.3': optional: true + '@rolldown/binding-darwin-arm64@1.1.2': + optional: true + '@rolldown/binding-darwin-x64@1.0.3': optional: true + '@rolldown/binding-darwin-x64@1.1.2': + optional: true + '@rolldown/binding-freebsd-x64@1.0.3': optional: true + '@rolldown/binding-freebsd-x64@1.1.2': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.1.2': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true + '@rolldown/binding-linux-arm64-gnu@1.1.2': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true + '@rolldown/binding-linux-arm64-musl@1.1.2': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.1.2': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true + '@rolldown/binding-linux-s390x-gnu@1.1.2': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true + '@rolldown/binding-linux-x64-gnu@1.1.2': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true + '@rolldown/binding-linux-x64-musl@1.1.2': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true + '@rolldown/binding-openharmony-arm64@1.1.2': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: '@emnapi/core': 1.10.0 @@ -758,16 +1953,99 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true + '@rolldown/binding-wasm32-wasi@1.1.2': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true + '@rolldown/binding-win32-arm64-msvc@1.1.2': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true + '@rolldown/binding-win32-x64-msvc@1.1.2': + optional: true + + '@rolldown/debug@1.1.2': {} + '@rolldown/pluginutils@1.0.1': {} '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.3.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.6 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.1 + + '@tailwindcss/oxide-android-arm64@4.3.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + optional: true + + '@tailwindcss/oxide@4.3.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-x64': 4.3.1 + '@tailwindcss/oxide-freebsd-x64': 4.3.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-x64-musl': 4.3.1 + '@tailwindcss/oxide-wasm32-wasi': 4.3.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 + + '@tailwindcss/vite@4.3.1(vite@8.0.16)': + dependencies: + '@tailwindcss/node': 4.3.1 + '@tailwindcss/oxide': 4.3.1 + tailwindcss: 4.3.1 + vite: 8.0.16(@types/node@25.9.1)(@vitejs/devtools@0.3.3)(jiti@2.7.0) + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 @@ -782,14 +2060,133 @@ snapshots: '@types/estree@1.0.9': {} + '@types/jsesc@2.5.1': {} + '@types/node@25.9.1': dependencies: undici-types: 7.24.6 - '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))': + '@valibot/to-json-schema@1.7.1(valibot@1.4.1(typescript@6.0.3))': + dependencies: + valibot: 1.4.1(typescript@6.0.3) + + '@vitejs/devtools-kit@0.3.3(typescript@6.0.3)(vite@8.0.16)': + dependencies: + '@devframes/hub': 0.5.4(devframe@0.5.4(typescript@6.0.3)) + birpc: 4.0.0 + devframe: 0.5.4(typescript@6.0.3) + mlly: 1.8.2 + nostics: 0.3.0 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + tinyexec: 1.2.4 + vite: 8.0.16(@types/node@25.9.1)(@vitejs/devtools@0.3.3)(jiti@2.7.0) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - crossws + - typescript + - utf-8-validate + + '@vitejs/devtools-rolldown@0.3.3(typescript@6.0.3)(vite@8.0.16)(vue@3.5.35(typescript@6.0.3))': + dependencies: + '@floating-ui/dom': 1.7.6 + '@rolldown/debug': 1.1.2 + '@vitejs/devtools-kit': 0.3.3(typescript@6.0.3)(vite@8.0.16) + birpc: 4.0.0 + cac: 7.0.0 + d3-shape: 3.2.0 + devframe: 0.5.4(typescript@6.0.3) + diff: 9.0.0 + get-port-please: 3.2.0 + h3: 2.0.1-rc.22 + mlly: 1.8.2 + mrmime: 2.0.1 + nostics: 0.3.0 + p-limit: 7.3.0 + pathe: 2.0.3 + publint: 0.3.21 + tinyglobby: 0.2.17 + unconfig: 7.5.0 + unstorage: 1.17.5 + vue-virtual-scroller: 3.0.4(vue@3.5.35(typescript@6.0.3)) + ws: 8.21.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@modelcontextprotocol/sdk' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - crossws + - db0 + - idb-keyval + - ioredis + - typescript + - uploadthing + - utf-8-validate + - vite + - vue + + '@vitejs/devtools@0.3.3(typescript@6.0.3)(vite@8.0.16)': + dependencies: + '@devframes/hub': 0.5.4(devframe@0.5.4(typescript@6.0.3)) + '@vitejs/devtools-kit': 0.3.3(typescript@6.0.3)(vite@8.0.16) + '@vitejs/devtools-rolldown': 0.3.3(typescript@6.0.3)(vite@8.0.16)(vue@3.5.35(typescript@6.0.3)) + birpc: 4.0.0 + cac: 7.0.0 + devframe: 0.5.4(typescript@6.0.3) + h3: 2.0.1-rc.22 + mlly: 1.8.2 + nostics: 0.3.0 + obug: 2.1.3 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + tinyexec: 1.2.4 + vite: 8.0.16(@types/node@25.9.1)(@vitejs/devtools@0.3.3)(jiti@2.7.0) + vue: 3.5.35(typescript@6.0.3) + ws: 8.21.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@modelcontextprotocol/sdk' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - crossws + - db0 + - idb-keyval + - ioredis + - typescript + - uploadthing + - utf-8-validate + + '@vitejs/plugin-vue@6.0.7(vite@8.0.16)(vue@3.5.35(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@25.9.1)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(@vitejs/devtools@0.3.3)(jiti@2.7.0) vue: 3.5.35(typescript@6.0.3) '@vitest/expect@4.1.8': @@ -801,13 +2198,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0))': + '@vitest/mocker@4.1.8(vite@8.0.16)': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@25.9.1)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(@vitejs/devtools@0.3.3)(jiti@2.7.0) '@vitest/pretty-format@4.1.8': dependencies: @@ -909,10 +2306,27 @@ snapshots: '@vue/shared@3.5.35': {} + acorn@8.17.0: {} + alien-signals@3.2.1: {} + ansis@4.3.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + assertion-error@2.0.1: {} + ast-kit@3.0.0: + dependencies: + '@babel/parser': 8.0.0 + estree-walker: 3.0.3 + pathe: 2.0.3 + + birpc@4.0.0: {} + c12@3.3.4: dependencies: chokidar: 5.0.0 @@ -928,26 +2342,70 @@ snapshots: pkg-types: 2.3.1 rc9: 3.0.1 + cac@7.0.0: {} + chai@6.2.2: {} chokidar@5.0.0: dependencies: readdirp: 5.0.0 + confbox@0.1.8: {} + confbox@0.2.4: {} convert-source-map@2.0.0: {} + cookie-es@1.2.3: {} + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + csstype@3.2.3: {} + d3-path@3.1.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + defu@6.1.7: {} destr@2.0.5: {} detect-libc@2.1.2: {} + devframe@0.5.4(typescript@6.0.3): + dependencies: + '@valibot/to-json-schema': 1.7.1(valibot@1.4.1(typescript@6.0.3)) + birpc: 4.0.0 + cac: 7.0.0 + h3: 2.0.1-rc.22 + mrmime: 2.0.1 + nostics: 0.2.0 + pathe: 2.0.3 + valibot: 1.4.1(typescript@6.0.3) + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - crossws + - typescript + - utf-8-validate + + diff@9.0.0: {} + dotenv@17.4.2: {} + dts-resolver@3.0.0: {} + + empathic@2.0.1: {} + + enhanced-resolve@5.21.6: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + entities@7.0.1: {} es-module-lexer@2.1.0: {} @@ -969,12 +2427,43 @@ snapshots: fsevents@2.3.3: optional: true + get-port-please@3.2.0: {} + + get-tsconfig@5.0.0-beta.5: + dependencies: + resolve-pkg-maps: 1.0.0 + giget@3.2.0: {} + graceful-fs@4.2.11: {} + + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + h3@2.0.1-rc.22: + dependencies: + rou3: 0.8.1 + srvx: 0.11.17 + hookable@6.1.1: {} + import-without-cache@0.4.0: {} + + iron-webcrypto@1.2.1: {} + jiti@2.7.0: {} + jsesc@3.1.0: {} + lightningcss-android-arm64@1.32.0: optional: true @@ -1024,20 +2513,113 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lru-cache@11.5.1: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mlly@1.8.2: + dependencies: + acorn: 8.17.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + mri@1.2.0: {} + mrmime@2.0.1: {} muggle-string@0.4.1: {} nanoid@3.3.12: {} + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.4: {} + + normalize-path@3.0.0: {} + + nostics@0.2.0: + dependencies: + magic-string: 0.30.21 + oxc-parser: 0.132.0 + unplugin: 3.0.0 + + nostics@0.3.0: + dependencies: + magic-string: 0.30.21 + oxc-parser: 0.132.0 + unplugin: 3.0.0 + obug@2.1.1: {} + obug@2.1.3: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + ohash@2.0.11: {} + oxc-parser@0.132.0: + dependencies: + '@oxc-project/types': 0.132.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.132.0 + '@oxc-parser/binding-android-arm64': 0.132.0 + '@oxc-parser/binding-darwin-arm64': 0.132.0 + '@oxc-parser/binding-darwin-x64': 0.132.0 + '@oxc-parser/binding-freebsd-x64': 0.132.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.132.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.132.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.132.0 + '@oxc-parser/binding-linux-arm64-musl': 0.132.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.132.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.132.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.132.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.132.0 + '@oxc-parser/binding-linux-x64-gnu': 0.132.0 + '@oxc-parser/binding-linux-x64-musl': 0.132.0 + '@oxc-parser/binding-openharmony-arm64': 0.132.0 + '@oxc-parser/binding-wasm32-wasi': 0.132.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.132.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.132.0 + '@oxc-parser/binding-win32-x64-msvc': 0.132.0 + + oxc-parser@0.137.0: + dependencies: + '@oxc-project/types': 0.137.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.137.0 + '@oxc-parser/binding-android-arm64': 0.137.0 + '@oxc-parser/binding-darwin-arm64': 0.137.0 + '@oxc-parser/binding-darwin-x64': 0.137.0 + '@oxc-parser/binding-freebsd-x64': 0.137.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.137.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.137.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.137.0 + '@oxc-parser/binding-linux-arm64-musl': 0.137.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.137.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-musl': 0.137.0 + '@oxc-parser/binding-openharmony-arm64': 0.137.0 + '@oxc-parser/binding-wasm32-wasi': 0.137.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.137.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.137.0 + '@oxc-parser/binding-win32-x64-msvc': 0.137.0 + + p-limit@7.3.0: + dependencies: + yocto-queue: 1.2.2 + + package-manager-detector@1.6.0: {} + path-browserify@1.0.1: {} pathe@2.0.3: {} @@ -1046,8 +2628,16 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + pkg-types@2.3.1: dependencies: confbox: 0.2.4 @@ -1060,6 +2650,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + publint@0.3.21: + dependencies: + '@publint/pack': 0.1.5 + package-manager-detector: 1.6.0 + picocolors: 1.1.1 + sade: 1.8.1 + + quansync@1.0.0: {} + + radix3@1.1.2: {} + rc9@3.0.1: dependencies: defu: 6.1.7 @@ -1067,6 +2668,25 @@ snapshots: readdirp@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + + rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.3(typescript@6.0.3)): + dependencies: + '@babel/generator': 8.0.0 + '@babel/helper-validator-identifier': 8.0.2 + '@babel/parser': 8.0.0 + ast-kit: 3.0.0 + birpc: 4.0.0 + dts-resolver: 3.0.0 + get-tsconfig: 5.0.0-beta.5 + obug: 2.1.3 + rolldown: 1.1.2 + optionalDependencies: + typescript: 6.0.3 + vue-tsc: 3.3.3(typescript@6.0.3) + transitivePeerDependencies: + - oxc-resolver + rolldown@1.0.3: dependencies: '@oxc-project/types': 0.133.0 @@ -1088,6 +2708,35 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.3 '@rolldown/binding-win32-x64-msvc': 1.0.3 + rolldown@1.1.2: + dependencies: + '@oxc-project/types': 0.137.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.2 + '@rolldown/binding-darwin-arm64': 1.1.2 + '@rolldown/binding-darwin-x64': 1.1.2 + '@rolldown/binding-freebsd-x64': 1.1.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.2 + '@rolldown/binding-linux-arm64-gnu': 1.1.2 + '@rolldown/binding-linux-arm64-musl': 1.1.2 + '@rolldown/binding-linux-ppc64-gnu': 1.1.2 + '@rolldown/binding-linux-s390x-gnu': 1.1.2 + '@rolldown/binding-linux-x64-gnu': 1.1.2 + '@rolldown/binding-linux-x64-musl': 1.1.2 + '@rolldown/binding-openharmony-arm64': 1.1.2 + '@rolldown/binding-wasm32-wasi': 1.1.2 + '@rolldown/binding-win32-arm64-msvc': 1.1.2 + '@rolldown/binding-win32-x64-msvc': 1.1.2 + + rou3@0.8.1: {} + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + semver@7.8.5: {} + siginfo@2.0.0: {} sirv@3.0.2: @@ -1098,10 +2747,16 @@ snapshots: source-map-js@1.2.1: {} + srvx@0.11.17: {} + stackback@0.0.2: {} std-env@4.1.0: {} + tailwindcss@4.3.1: {} + + tapable@2.3.3: {} + tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -1115,6 +2770,35 @@ snapshots: totalist@3.0.1: {} + tree-kill@1.2.2: {} + + tsdown@0.22.3(@vitejs/devtools@0.3.3)(publint@0.3.21)(typescript@6.0.3)(vue-tsc@3.3.3(typescript@6.0.3)): + dependencies: + ansis: 4.3.1 + cac: 7.0.0 + defu: 6.1.7 + empathic: 2.0.1 + hookable: 6.1.1 + import-without-cache: 0.4.0 + obug: 2.1.3 + picomatch: 4.0.4 + rolldown: 1.1.2 + rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.3(typescript@6.0.3)) + semver: 7.8.5 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + optionalDependencies: + '@vitejs/devtools': 0.3.3(typescript@6.0.3)(vite@8.0.16) + publint: 0.3.21 + typescript: 6.0.3 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - vue-tsc + tslib@2.8.1: optional: true @@ -1122,9 +2806,45 @@ snapshots: ufo@1.6.4: {} + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.7 + jiti: 2.7.0 + quansync: 1.0.0 + unconfig-core: 7.5.0 + + uncrypto@0.1.3: {} + undici-types@7.24.6: {} - vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0): + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + unstorage@1.17.5: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.5.1 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + + valibot@1.4.1(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + + vite@8.0.16(@types/node@25.9.1)(@vitejs/devtools@0.3.3)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -1133,13 +2853,14 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.9.1 + '@vitejs/devtools': 0.3.3(typescript@6.0.3)(vite@8.0.16) fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.8(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0)): + vitest@4.1.8(@types/node@25.9.1)(vite@8.0.16): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(jiti@2.7.0)) + '@vitest/mocker': 4.1.8(vite@8.0.16) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -1156,7 +2877,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@25.9.1)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(@vitejs/devtools@0.3.3)(jiti@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.1 @@ -1171,6 +2892,10 @@ snapshots: '@vue/language-core': 3.3.3 typescript: 6.0.3 + vue-virtual-scroller@3.0.4(vue@3.5.35(typescript@6.0.3)): + dependencies: + vue: 3.5.35(typescript@6.0.3) + vue@3.5.35(typescript@6.0.3): dependencies: '@vue/compiler-dom': 3.5.35 @@ -1181,7 +2906,13 @@ snapshots: optionalDependencies: typescript: 6.0.3 + webpack-virtual-modules@0.6.2: {} + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + + ws@8.21.0: {} + + yocto-queue@1.2.2: {} diff --git a/vite-layers/src/config.ts b/vite-layers/src/config.ts index 19cda7d..c1c9dbe 100644 --- a/vite-layers/src/config.ts +++ b/vite-layers/src/config.ts @@ -3,18 +3,12 @@ import { loadConfig, type ConfigLayer } from 'c12' import { createDefu } from 'defu' import { glob } from 'tinyglobby' import { withoutTrailingSlash, withTrailingSlash } from 'ufo' -import type { Layer, LayerConfig, LayerStack } from './types' +import type { Layer, LayerConfig, LayerEdge, LayerStack } from './types' +import { toPosix } from './util' /** Identity helper for typed `app.config.ts` files. */ export const defineLayerConfig = (config: LayerConfig): LayerConfig => config -/** - * Normalize to forward slashes. c12 returns `cwd` posix-style while node `resolve()` is - * OS-native (backslashes on Windows); paths must be canonicalized before they are compared - * for dedup or emitted into a Vite config (where posix is conventional). - */ -const toPosix = (p: string) => p.replace(/\\/g, '/') - /** * Port of Nuxt's layer merger: arrays are concatenated rather than replaced. * (See `@nuxt/kit` `loadNuxtConfig`.) @@ -59,6 +53,12 @@ export async function resolveLayerStack( // 2) Cycle-guard [improvement]: terminate the recursion on a repeated source. const seen = new Set() + // Capture the extends DAG as c12 walks it. c12 consumes (strips) the `extends`/`_extends` keys from + // each resolved layer's config — only the project's survive — so the parent→child edges can't be + // reconstructed from the resolved configs afterwards. The `resolve` hook fires once per extend edge + // (incl. nested, diamond, and auto-scanned `_extends`), with `opts.cwd` = the extending layer's dir. + const edges: LayerEdge[] = [] + const { config, layers = [] } = await loadConfig({ cwd, configFile: 'app.config', @@ -71,8 +71,11 @@ export async function resolveLayerStack( packageJson: false, globalRc: false, merger: merger as (...sources: Array) => LayerConfig, - resolve(id, opts) { - const abs = resolve(opts?.cwd ?? cwd, id) + resolve(id, ropts) { + const from = toPosix(withoutTrailingSlash(ropts?.cwd ?? cwd)) + const abs = resolve(ropts?.cwd ?? cwd, id) + const to = toPosix(withoutTrailingSlash(abs)) + if (to !== from) edges.push({ from, to, source: id }) // skip c12's self-resolution of the root if (seen.has(abs)) return { config: {}, cwd: abs } seen.add(abs) return undefined @@ -101,5 +104,5 @@ export async function resolveLayerStack( stack.push({ rootDir, srcDir, name: name ?? basename(rootDir), config: layer.config ?? {} }) } - return { merged: config, layers: stack } + return { merged: config, layers: stack, edges } } diff --git a/vite-layers/src/dev.ts b/vite-layers/src/dev.ts index 1605bdb..e17d56a 100644 --- a/vite-layers/src/dev.ts +++ b/vite-layers/src/dev.ts @@ -1,84 +1,41 @@ -import { statSync } from 'node:fs' import { resolve } from 'node:path' -import MagicString from 'magic-string' import type { Plugin } from 'vite' +import { toPosix } from './util' const CONFIG_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'] -const toPosix = (p: string) => p.replace(/\\/g, '/') - -const existingConfigFiles = (rootDirs: string[]): Set => { - const files = new Set() - for (const dir of rootDirs) { - for (const ext of CONFIG_EXTENSIONS) { - const file = resolve(dir, `app.config${ext}`) - try { - if (statSync(file).isFile()) files.add(toPosix(file)) - } catch { - // not present in this layer - } - } - } - return files -} /** * Dev-only plugin: restart the Vite server when any layer's `app.config.*` changes. * * `app.config.ts` is loaded out-of-band by c12 (not part of Vite's module graph or config-file * dependencies), so Vite never restarts on its own when you edit feature flags / layer config — the - * baked `__FEATURES__` `define` and aliases go stale. We watch each resolved layer's config file - * (including layers outside the project root via `watcher.add`) and call `server.restart()`, which - * re-runs `buildViteConfig` → `resolveLayerStack` (c12 reads fresh) → new `define`. + * `feature()` values baked into transformed modules and the resolved aliases go stale. We watch each + * resolved layer's config file (including layers outside the project root via `watcher.add`) and call + * `server.restart()`, which re-runs `buildViteConfig` → `resolveLayerStack` (c12 reads fresh) → a new + * `featurePlugin` with the updated flag values. */ export function configWatchPlugin(rootDirs: string[]): Plugin { return { name: 'vite-layers:config-watch', apply: 'serve', configureServer(server) { - const files = existingConfigFiles(rootDirs) - if (files.size === 0) return - server.watcher.add([...files]) // ensure extended layers outside the root are watched too + // Every POSSIBLE `app.config.*` path (existing or not), so creating a config in a layer that + // had none — or deleting one — also restarts, not just edits to configs present at startup. + const candidates = new Set() + for (const dir of rootDirs) { + for (const ext of CONFIG_EXTENSIONS) candidates.add(toPosix(resolve(dir, `app.config${ext}`))) + } + if (candidates.size === 0) return + // chokidar watches an absent path for creation too, so `add`/`unlink` fire for a config that + // appears/disappears later (incl. layers outside the project root). + server.watcher.add([...candidates]) - const onChange = (file: string) => { - if (!files.has(toPosix(file))) return + const onEvent = (file: string) => { + if (!candidates.has(toPosix(file))) return server.config.logger.info('[vite-layers] app config changed — restarting…', { timestamp: true }) void server.restart() } - server.watcher.on('change', onChange) - }, - } -} - -/** Matches a standalone `__FEATURES__` reference (not a `.__FEATURES__` property access). */ -const STANDALONE_FEATURES_RE = /(?`, not in template expressions. - */ -export function featuresRuntimePlugin(features: Record = {}): Plugin { - const json = JSON.stringify(features) - return { - name: 'vite-layers:features-runtime', - apply: 'serve', - transform(code, id) { - if (id.includes('/node_modules/') || !STANDALONE_FEATURES_RE.test(code)) return null - // NOTE: rolldown's *native* magic-string (the transform `meta.magicString` in the rolldown - // docs) is NOT surfaced by Vite plugins — `meta` is `{ inMap, moduleType, ssr }` with no - // `magicString` in dev or build. So we use the npm `magic-string` fallback the rolldown docs - // recommend for non-native hosts; it also produces clean cross-platform sourcemaps. - // Prepend on line 1 (keeps line numbers); module-local const shadows the missing global. - const s = new MagicString(code) - s.prepend(`const __FEATURES__=${json};`) - return { code: s.toString(), map: s.generateMap({ source: id, hires: true }) } + for (const event of ['add', 'change', 'unlink'] as const) server.watcher.on(event, onEvent) }, } } diff --git a/vite-layers/src/devtools.ts b/vite-layers/src/devtools.ts new file mode 100644 index 0000000..79ee9b2 --- /dev/null +++ b/vite-layers/src/devtools.ts @@ -0,0 +1,795 @@ +import { readdirSync, statSync } from 'node:fs' +import { join, relative, resolve } from 'node:path' +import type { ConfigEnv } from 'vite' +import type { + DevToolsServerCommandInput, + DevToolsViewGroup, + DevToolsViewJsonRender, + JsonRenderElement, + JsonRenderSpec, + PluginWithDevTools, + ViteDevToolsNodeContext, +} from '@vitejs/devtools-kit' +import { flattenFeatures } from './features' +import type { LayeredResolution, ResolveRecord } from './resolve' +import { generateTsConfig, type GenerateTsConfigOptions } from './tsconfig' +import type { LayerStack } from './types' +import { toPosix } from './util' + +// --------------------------------------------------------------------------------------------- +// This module imports **only types** from `@vitejs/devtools-kit` — they are erased at emit, so the +// plugin has no runtime dependency on the kit and is fully inert unless the `@vitejs/devtools` hub +// mounts it and calls `setup`. The kit's `defineRpcFunction`/`defineDockEntry`/`defineCommand` are +// pure identity helpers and `register()` accepts plain objects, so we hand-build the (typed) specs. +// +// We import the real kit types (rather than re-declaring a local subset) on purpose: when the hub is +// present it augments `vite`'s `Plugin` with `devtools`, and the real types keep our `setup` +// signature consistent with that augmentation. The kit ships transitively with `@vitejs/devtools`, so +// any project using these panels already has it; type-checking vite-layers needs the kit present +// (an optional peer in the install sense, required in the type-check sense for this raw-source pkg). +// --------------------------------------------------------------------------------------------- + +const NS = 'vite-layers' +const GROUP_ID = NS +const PANEL = { + layers: `${NS}:layers`, + features: `${NS}:features`, + resolver: `${NS}:resolver`, + assets: `${NS}:assets`, +} as const +const RPC = { + refresh: `${NS}:refresh`, + resolve: `${NS}:resolve`, + clearLog: `${NS}:clear-log`, +} as const +const ICON = { + group: 'ph:stack-duotone', + layers: 'ph:stack-duotone', + features: 'ph:toggle-right-duotone', + resolver: 'ph:signpost-duotone', + assets: 'ph:images-duotone', + refresh: 'ph:arrows-clockwise-duotone', +} as const + +/** Data the devtools panel needs, captured by `buildViteConfig` at config-resolution time. */ +export interface LayersDevtoolsData { + /** The app directory (the cwd `resolveLayerStack` was called with). */ + appDir: string + /** The Vite env (`command`/`mode`) the config was built for. */ + env: ConfigEnv + /** The resolved, in-effect layer stack (after `layers:resolved` hooks). */ + stack: LayerStack + /** The live layered resolution — shared with `vite-layers:resolve` so the panel sees real data. */ + resolution: LayeredResolution + /** tsconfig generation options, or `false` when autogen is disabled. */ + tsconfig: GenerateTsConfigOptions | false +} + +// --------------------------------------------------------------------------------------------- +// Snapshot — a plain, serializable view of the stack the spec builders render. Cheap to recompute, +// so `vite-layers:refresh` just rebuilds it (re-walking `public/`, re-reading the resolution log). +// --------------------------------------------------------------------------------------------- + +interface LayerRow { + index: number + name: string + project: boolean + extends: string + rootDir: string + srcDir: string +} +interface FeatureRow { + key: string + value: string + type: string + kind: 'leaf' | 'group' + enabled: boolean +} +// These two row shapes are handed to `DataTable` as-is (not re-mapped at the call site), so they +// carry an index signature to satisfy the renderer's `Record` row type. +interface PublicRow { + path: string + winner: string + shadowedBy: string + [key: string]: unknown +} +interface HookRow { + hook: string + layer: string + [key: string]: unknown +} +type TsconfigInfo = + | { enabled: false } + | { enabled: true; paths: Record; appJson: string; nodeJson: string; dts: string } + +interface Snapshot { + projectName: string + appDir: string + mode: string + command: string + layers: LayerRow[] + mergedTree: Record + features: FeatureRow[] + rawFeatures: Record + featureLeafCount: number + featureDisabledCount: number + publicAssets: PublicRow[] + publicLayerCount: number + hooks: HookRow[] + tsconfig: TsconfigInfo + inheritanceTree: string +} + +/** Recursively list files under a directory (absolute paths); `[]` if it isn't a directory. */ +function walk(dir: string, out: string[] = []): string[] { + let entries: string[] + try { + entries = readdirSync(dir) + } catch { + return out + } + for (const name of entries) { + const abs = join(dir, name) + // Guard each stat: a broken symlink or a file unlinked between readdir and stat (a real TOCTOU + // window under the dev watcher) throws ENOENT — skip it instead of failing the whole snapshot. + let isDir: boolean + try { + isDir = statSync(abs).isDirectory() + } catch { + continue + } + if (isDir) walk(abs, out) + else out.push(abs) + } + return out +} + +const asArray = (v: unknown): string[] => + v == null ? [] : (Array.isArray(v) ? v : [v]).map(String) + +const TRAILING_SLASHES_RE = /\/+$/ +const noTrailing = (p: string) => p.replace(TRAILING_SLASHES_RE, '') + +/** + * Render the layer **extends graph** as a box-drawing tree (for a monospace CodeBlock). + * + * The resolved stack is a flat priority order; the *structure* comes from `stack.edges` — the + * parent→child edges captured during resolution (c12 strips the `extends` keys from resolved configs, + * so they can't be read back afterwards). Diamonds (a layer reached via two parents) are drawn once + * and marked `↑ above` on repeat — no infinite recursion. Edges whose target isn't a stack layer (npm + * / git sources) become `(external)` leaves, and any layer never reached from the project is listed + * below so the view stays complete. With no `edges` (a hand-built stack) only the project is drawn. + */ +export function inheritanceTreeText(stack: LayerStack): string { + const { layers, edges = [] } = stack + const byRoot = new Map() + layers.forEach((l, i) => byRoot.set(noTrailing(toPosix(l.rootDir)), i)) + + // Group edges by the (normalized) directory they extend FROM, preserving walk order. + const childEdges = new Map>() + for (const e of edges) { + const fromKey = noTrailing(toPosix(e.from)) + const idx = byRoot.get(noTrailing(toPosix(e.to))) + const list = childEdges.get(fromKey) ?? childEdges.set(fromKey, []).get(fromKey)! + list.push(idx !== undefined ? { index: idx } : { external: e.source }) + } + const childrenOf = (i: number) => childEdges.get(noTrailing(toPosix(layers[i]!.rootDir))) ?? [] + + const lines: string[] = [] + const seen = new Set() + + const render = (i: number, prefix: string, isLast: boolean, isRoot: boolean) => { + const connector = isRoot ? '' : isLast ? '└── ' : '├── ' + const repeated = seen.has(i) + const tag = isRoot ? ' (project · highest priority)' : '' + lines.push(`${prefix}${connector}${layers[i]!.name} #${i}${tag}${repeated ? ' ↑ above' : ''}`) + if (repeated) return + seen.add(i) + + const kids = childrenOf(i) + const childPrefix = prefix + (isRoot ? '' : isLast ? ' ' : '│ ') + kids.forEach((k, ci) => { + const last = ci === kids.length - 1 + if ('index' in k) render(k.index, childPrefix, last, false) + else lines.push(`${childPrefix}${last ? '└── ' : '├── '}${k.external} (external)`) + }) + } + + render(0, '', true, true) + + const orphans = layers.map((_, i) => i).filter(i => !seen.has(i)) + if (orphans.length) { + lines.push('') + lines.push('not reached via extends (e.g. auto-scanned layers/*):') + for (const i of orphans) lines.push(`• ${layers[i]!.name} #${i}`) + } + + return lines.join('\n') +} + +async function collectSnapshot(data: LayersDevtoolsData): Promise { + const { layers, merged } = data.stack + + const layerRows: LayerRow[] = layers.map((l, index) => ({ + index, + name: l.name, + project: index === 0, + extends: asArray(l.config.extends).join(', ') || '—', + rootDir: l.rootDir, + srcDir: l.srcDir, + })) + + // Features: every dotted path (groups + leaves). Leaves drive DCE; a falsy leaf is the value the + // `feature()` macro folds to `false`, killing its branch + chunk. + const flat = flattenFeatures((merged.features ?? {}) as Record) + const features: FeatureRow[] = flat.map(([key, value]) => { + const group = value != null && typeof value === 'object' + return { + key, + value: JSON.stringify(value) ?? String(value), + type: Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value, + kind: group ? 'group' : 'leaf', + enabled: Boolean(value), + } + }) + const leaves = features.filter(f => f.kind === 'leaf') + + // Public assets: walk each layer's `public/` high→low; the first layer to hold a path wins, the + // rest are shadowed — mirrors `publicLayersPlugin`'s first-match-by-priority resolution. + const publicLayers = layers + .map(l => ({ name: l.name, dir: resolve(l.rootDir, 'public') })) + .filter(p => { + try { + return statSync(p.dir).isDirectory() + } catch { + return false + } + }) + const byPath = new Map() + for (const { name, dir } of publicLayers) { + for (const abs of walk(dir)) { + const rel = toPosix(relative(dir, abs)) + ;(byPath.get(rel) ?? byPath.set(rel, []).get(rel)!).push(name) + } + } + const publicAssets: PublicRow[] = [...byPath.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([path, names]) => ({ + path, + winner: names[0]!, + shadowedBy: names.slice(1).join(', ') || '—', + })) + + // Per-layer lifecycle hooks (base-first display order, like registration). + const hooks: HookRow[] = [] + for (const l of [...layers].reverse()) { + for (const hook of Object.keys(l.config.hooks ?? {})) hooks.push({ hook, layer: l.name }) + } + + // Curated merged view for the Tree — omit `vite` (functions/plugins) and `hooks` (functions), + // which aren't serializable and aren't useful as a tree. + const mergedTree: Record = { + name: merged.name, + extends: merged.extends, + srcDir: merged.srcDir, + features: merged.features ?? {}, + tsConfig: merged.tsConfig, + vite: merged.vite ? '[Vite config fragment — see Vite DevTools]' : undefined, + hooks: hooks.length ? hooks.map(h => `${h.hook} (${h.layer})`) : undefined, + } + + let tsconfig: TsconfigInfo = { enabled: false } + if (data.tsconfig !== false) { + const opts = typeof data.tsconfig === 'object' ? data.tsconfig : {} + const gen = await generateTsConfig(data.appDir, { ...opts, stack: data.stack }) + tsconfig = { + enabled: true, + paths: (gen.tsconfig.compilerOptions?.paths ?? {}) as Record, + appJson: JSON.stringify(gen.tsconfig, null, 2), + nodeJson: JSON.stringify(gen.nodeTsconfig, null, 2), + dts: gen.dts, + } + } + + return { + projectName: layers[0]?.name ?? 'app', + appDir: data.appDir, + mode: data.env.mode, + command: data.env.command, + layers: layerRows, + mergedTree, + features, + rawFeatures: (merged.features ?? {}) as Record, + featureLeafCount: leaves.length, + featureDisabledCount: leaves.filter(f => !f.enabled).length, + publicAssets, + publicLayerCount: publicLayers.length, + hooks, + tsconfig, + inheritanceTree: inheritanceTreeText(data.stack), + } +} + +/** Which layer (by name) owns a resolved file — found by matching the file against layer `srcDir`s. */ +function layerOf(stack: LayerStack, file: string | null): string { + if (!file) return '—' + const f = toPosix(file.split('?')[0]!) + for (const l of stack.layers) { + const src = toPosix(l.srcDir) + if (f === src || f.startsWith(`${src}/`)) return l.name + } + return '?' +} + +// --------------------------------------------------------------------------------------------- +// Spec builder — a tiny DSL over the flat `{ root, elements }` json-render shape. Each `add*` +// returns the generated element id, so panels compose by nesting calls. +// --------------------------------------------------------------------------------------------- + +interface Column { + key: string + label: string + width?: string +} + +class Spec { + private readonly elements: Record = {} + private readonly state: Record = {} + private n = 0 + + private add(node: JsonRenderElement): string { + const id = `e${this.n++}` + this.elements[id] = node + return id + } + + setState(key: string, value: unknown): this { + this.state[key] = value + return this + } + + vstack(children: string[], gap = 12, padding?: number): string { + return this.add({ type: 'Stack', props: { direction: 'vertical', gap, padding }, children }) + } + + hstack(children: string[], props: Record = {}): string { + return this.add({ type: 'Stack', props: { direction: 'horizontal', gap: 8, align: 'center', ...props }, children }) + } + + card(title: string, children: string[], collapsible = false): string { + return this.add({ type: 'Card', props: { title, collapsible }, children }) + } + + text(content: string, variant?: 'heading' | 'body' | 'caption' | 'code'): string { + return this.add({ type: 'Text', props: { content, variant } }) + } + + badge(text: string, variant: 'default' | 'info' | 'success' | 'warning' | 'error' = 'default', title?: string): string { + return this.add({ type: 'Badge', props: { text, variant, title } }) + } + + divider(label?: string): string { + return this.add({ type: 'Divider', props: { label } }) + } + + kvTable(entries: Array<{ key: string; value: string }>, title?: string): string { + return this.add({ type: 'KeyValueTable', props: { title, entries } }) + } + + dataTable(columns: Column[], rows: Array>, maxHeight = '360px'): string { + return this.add({ type: 'DataTable', props: { columns, rows, maxHeight } }) + } + + tree(data: unknown, expandLevel = 1): string { + return this.add({ type: 'Tree', props: { data, expandLevel } }) + } + + code(code: string, filename?: string, maxHeight = '320px'): string { + return this.add({ type: 'CodeBlock', props: { code, filename, maxHeight } }) + } + + button(label: string, action: string, opts: { icon?: string; variant?: string; params?: Record } = {}): string { + return this.add({ + type: 'Button', + props: { label, icon: opts.icon, variant: opts.variant ?? 'secondary' }, + on: { press: { action, params: opts.params } }, + }) + } + + textInput(stateKey: string, placeholder: string): string { + return this.add({ type: 'TextInput', props: { placeholder, value: { $bindState: `/${stateKey}` } } }) + } + + build(root: string): JsonRenderSpec { + return { root, elements: this.elements, state: this.state } + } +} + +/** A header row: a heading on the left, a Refresh button on the right. */ +function header(s: Spec, title: string, subtitle: string): string { + const left = s.vstack([s.text(title, 'heading'), s.text(subtitle, 'caption')], 2) + const refresh = s.button('Refresh', RPC.refresh, { icon: ICON.refresh }) + return s.hstack([left, refresh], { justify: 'space-between' }) +} + +// --------------------------------------------------------------------------------------------- +// Panels +// --------------------------------------------------------------------------------------------- + +function buildLayersSpec(snap: Snapshot): JsonRenderSpec { + const s = new Spec() + const sections: string[] = [ + header(s, 'Layers', `${snap.layers.length} layers · ${snap.projectName} · ${snap.command}/${snap.mode}`), + ] + + // Headline visual: the extends graph drawn as a tree (the structure the flat stack flattens away). + sections.push( + s.card('Inheritance (extends graph)', [ + s.code(snap.inheritanceTree, 'extends graph'), + s.text('Reconstructed from each layer’s extends. Diamonds drawn once (↑ above); external (npm/git) sources marked.', 'caption'), + ]), + ) + + sections.push( + s.card('Layer stack (high → low priority)', [ + s.dataTable( + [ + { key: 'index', label: '#', width: '36px' }, + { key: 'name', label: 'Name' }, + { key: 'role', label: 'Role', width: '90px' }, + { key: 'extends', label: 'Extends' }, + { key: 'srcDir', label: 'srcDir' }, + ], + snap.layers.map(l => ({ + index: l.index, + name: l.name, + role: l.project ? 'project' : 'layer', + extends: l.extends, + srcDir: l.srcDir, + })), + ), + s.text('layers[0] is the project (highest priority); collisions resolve to the smaller index.', 'caption'), + ]), + ) + + sections.push(s.card('Merged config', [s.tree(prune(snap.mergedTree), 2)], true)) + + if (snap.hooks.length) { + sections.push( + s.card( + 'Lifecycle hooks', + [ + s.dataTable( + [ + { key: 'hook', label: 'Hook' }, + { key: 'layer', label: 'Declared by' }, + ], + snap.hooks, + '200px', + ), + s.text('Hooks run serially, base layer first.', 'caption'), + ], + true, + ), + ) + } + + return s.build(s.vstack(sections, 14, 12)) +} + +function buildFeaturesSpec(snap: Snapshot): JsonRenderSpec { + const s = new Spec() + const sections: string[] = [ + header( + s, + 'Features', + `${snap.featureLeafCount} flags · ${snap.featureDisabledCount} disabled (dead-code eliminated)`, + ), + ] + + if (snap.features.length === 0) { + sections.push(s.text('No feature flags defined in any layer.', 'caption')) + } else { + sections.push( + s.card('Flags (merged, high → low priority)', [ + s.dataTable( + [ + { key: 'status', label: '', width: '30px' }, + { key: 'key', label: 'Key' }, + { key: 'value', label: 'Value' }, + { key: 'type', label: 'Type', width: '70px' }, + { key: 'dce', label: 'feature()', width: '150px' }, + ], + snap.features.map(f => ({ + status: f.kind === 'group' ? '▸' : f.enabled ? '●' : '○', + key: f.key, + value: f.value, + type: f.type, + dce: + f.kind === 'group' + ? '(group)' + : f.enabled + ? 'kept' + : 'branch eliminated', + })), + ), + ]), + ) + sections.push(s.card('Raw feature tree', [s.tree(snap.rawFeatures, 3)], true)) + } + + sections.push( + s.card( + 'About dead-code elimination', + [ + s.text( + "feature('key') is replaced by the flag's literal at compile time (dev + build alike). " + + 'A disabled flag folds to false, so its branch — and any import() inside it — is statically ' + + 'dead and the chunk is never emitted. An unknown key fails the build.', + 'caption', + ), + ], + true, + ), + ) + + return s.build(s.vstack(sections, 14, 12)) +} + +interface ResolveResult { + id: string + sub: string + query: string + candidates: string[] + error?: string +} + +function buildResolverSpec(data: LayersDevtoolsData, query: string, result: ResolveResult | null): JsonRenderSpec { + const s = new Spec() + s.setState('query', query) + + const sections: string[] = [ + s.vstack([s.text('Resolver', 'heading'), s.text(`Prefixes: ${data.resolution.prefixes.join(' ')}`, 'caption')], 2), + ] + + // Playground: type a layered id, resolve it across the stack. + const input = s.textInput('query', 'e.g. @/components/AppHeader.vue') + const go = s.button('Resolve', RPC.resolve, { icon: ICON.resolver, variant: 'primary', params: { id: { $state: '/query' } } }) + sections.push(s.card('Playground', [s.hstack([input, go]), ...resolveResultEls(s, data, result)])) + + // Live log of real @/ ~/ resolutions seen this session. + const records = data.resolution.records() + const logChildren: string[] = [ + s.hstack([s.text(`Live resolutions (${records.length})`, 'body'), s.button('Clear', RPC.clearLog, { icon: 'ph:eraser-duotone' })], { + justify: 'space-between', + }), + ] + if (records.length === 0) { + logChildren.push(s.text('No layered imports resolved yet — load the app to populate this.', 'caption')) + } else { + logChildren.push(s.dataTable( + [ + { key: 'id', label: 'Import' }, + { key: 'resolves', label: 'Resolves to (layer)', width: '150px' }, + { key: 'via', label: 'Via', width: '120px' }, + { key: 'n', label: '#cand', width: '60px' }, + ], + records.map(r => ({ + id: r.id, + resolves: layerOf(data.stack, r.resolved), + via: recordVia(r), + n: r.candidates.length, + })), + '300px', + )) + } + sections.push(s.card('Live log', logChildren)) + + return s.build(s.vstack(sections, 14, 12)) +} + +/** Describe how a record resolved: a normal import, a `super()` self-import, or unresolved. */ +function recordVia(r: ResolveRecord): string { + if (r.resolved === null) return 'unresolved' + if (r.selfIndex < 0) return 'top match' + return `super() #${r.selfIndex + 1}` +} + +function resolveResultEls(s: Spec, data: LayersDevtoolsData, result: ResolveResult | null): string[] { + if (!result) return [s.text('Enter a layered import above and press Resolve.', 'caption')] + if (result.error) return [s.badge(result.error, 'error')] + if (result.candidates.length === 0) { + return [s.badge(`No file matches "${result.id}" in any layer.`, 'warning')] + } + const winner = result.candidates[0]! + return [ + s.hstack([s.text('Resolves to', 'caption'), s.badge(layerOf(data.stack, winner), 'success', winner)]), + s.dataTable( + [ + { key: 'pri', label: '#', width: '36px' }, + { key: 'status', label: '', width: '90px' }, + { key: 'layer', label: 'Layer', width: '110px' }, + { key: 'file', label: 'File' }, + ], + result.candidates.map((file, i) => ({ + pri: i, + status: i === 0 ? 'winner' : 'shadowed', + layer: layerOf(data.stack, file), + file, + })), + '240px', + ), + s.text('A self-import (an override importing its own path) would super()-skip to the next row down.', 'caption'), + ] +} + +function buildAssetsSpec(snap: Snapshot): JsonRenderSpec { + const s = new Spec() + const sections: string[] = [ + header(s, 'Public & TS', `${snap.publicAssets.length} assets across ${snap.publicLayerCount} public/ dirs`), + ] + + const publicChildren: string[] = + snap.publicAssets.length === 0 + ? [s.text('No layer has a public/ directory.', 'caption')] + : [ + s.dataTable( + [ + { key: 'path', label: 'Asset' }, + { key: 'winner', label: 'Served from', width: '120px' }, + { key: 'shadowedBy', label: 'Shadows', width: '140px' }, + ], + snap.publicAssets, + '260px', + ), + s.text('Higher-priority layers win; the winner is served in dev and emitted to the build output.', 'caption'), + ] + sections.push(s.card('Layered public/ assets', publicChildren)) + + if (snap.tsconfig.enabled) { + const ts = snap.tsconfig + sections.push( + s.card( + 'Generated tsconfig paths', + [ + s.kvTable( + Object.entries(ts.paths).map(([key, value]) => ({ key, value: value.join(' • ') })), + ), + s.text('@/ and ~/ map to every layer srcDir in priority order — tsc mirrors the runtime resolver.', 'caption'), + ], + ), + ) + sections.push(s.card('.vite-layers/tsconfig.json', [s.code(ts.appJson, 'tsconfig.json')], true)) + sections.push(s.card('.vite-layers/tsconfig.node.json', [s.code(ts.nodeJson, 'tsconfig.node.json')], true)) + sections.push(s.card('.vite-layers/features.d.ts', [s.code(ts.dts, 'features.d.ts')], true)) + } else { + sections.push(s.card('TypeScript', [s.text('tsconfig autogeneration is disabled (tsconfig: false).', 'caption')])) + } + + return s.build(s.vstack(sections, 14, 12)) +} + +/** Drop `undefined` values so the merged-config Tree stays tidy. */ +function prune(obj: Record): Record { + const out: Record = {} + for (const [k, v] of Object.entries(obj)) if (v !== undefined) out[k] = v + return out +} + +// --------------------------------------------------------------------------------------------- +// Plugin +// --------------------------------------------------------------------------------------------- + +/** Build an `action`-type RPC definition. `register` takes the plain object (the kit's + * `defineRpcFunction` is identity); the parameter type is a broad conditional, so cast once here. */ +type RpcDefinition = Parameters[0] +const action = (name: string, handler: (params?: Record) => void | Promise): RpcDefinition => + ({ name, type: 'action', setup: () => ({ handler }) }) as unknown as RpcDefinition + +/** + * Run the layered resolver against a single id, for the playground. Uses `parse` + `candidates` + * directly (not `resolveId`) so a manual query doesn't pollute the live log, and so we can show the + * full candidate stack rather than just the winner. + */ +function runResolve(data: LayersDevtoolsData, rawId: unknown): ResolveResult { + const id = String(rawId ?? '').trim() + const parsed = data.resolution.parse(id) + if (!id) return { id, sub: '', query: '', candidates: [], error: 'Enter an import id.' } + if (!parsed) { + return { + id, + sub: '', + query: '', + candidates: [], + error: `Not a layered id — must start with one of: ${data.resolution.prefixes.join(', ')}`, + } + } + return { id, sub: parsed.sub, query: parsed.query, candidates: data.resolution.candidates(parsed.sub) } +} + +/** + * The vite-layers DevTools integration: four json-render panels (Layers, Features, Resolver, Public & + * TS) grouped under a single dock button, plus a refresh command and an init message. Server-rendered + * JSON specs — no client bundle, keeping vite-layers buildless. Inert unless the `@vitejs/devtools` + * hub mounts it; `buildViteConfig` attaches it by default (disable with `devtools: false`). + */ +export function layersDevtoolsPlugin(data: LayersDevtoolsData): PluginWithDevTools { + return { + name: 'vite-layers:devtools', + devtools: { + async setup(ctx: ViteDevToolsNodeContext) { + let snap = await collectSnapshot(data) + let lastQuery = '' + let lastResult: ResolveResult | null = null + + const layersUi = ctx.createJsonRenderer(buildLayersSpec(snap)) + const featuresUi = ctx.createJsonRenderer(buildFeaturesSpec(snap)) + const resolverUi = ctx.createJsonRenderer(buildResolverSpec(data, lastQuery, lastResult)) + const assetsUi = ctx.createJsonRenderer(buildAssetsSpec(snap)) + + // A single dock button collapsing the four panels (orphan-tolerant: if the host doesn't + // render groups, the entries fall back to top-level — no loss of access). + ctx.docks.register({ id: GROUP_ID, type: 'group', title: 'vite-layers', icon: ICON.group, category: 'app' } satisfies DevToolsViewGroup) + + const entry = (id: string, title: string, icon: string, ui: typeof layersUi, order: number) => { + // Typed as the full entry interface (not the narrow literal) so the returned handle's + // `update(patch)` accepts base-entry fields like `badge`. + const view: DevToolsViewJsonRender = { + id, + title, + icon, + type: 'json-render', + ui, + groupId: GROUP_ID, + category: 'app', + defaultOrder: order, + } + return ctx.docks.register(view) + } + + entry(PANEL.layers, 'Layers', ICON.layers, layersUi, 40) + entry(PANEL.resolver, 'Resolver', ICON.resolver, resolverUi, 30) + entry(PANEL.assets, 'Public & TS', ICON.assets, assetsUi, 20) + const featuresEntry = entry(PANEL.features, 'Features', ICON.features, featuresUi, 35) + const featuresBadge = () => (snap.featureDisabledCount > 0 ? String(snap.featureDisabledCount) : undefined) + featuresEntry.update({ badge: featuresBadge() }) + + const refresh = async () => { + snap = await collectSnapshot(data) + await Promise.all([ + layersUi.updateSpec(buildLayersSpec(snap)), + featuresUi.updateSpec(buildFeaturesSpec(snap)), + assetsUi.updateSpec(buildAssetsSpec(snap)), + resolverUi.updateSpec(buildResolverSpec(data, lastQuery, lastResult)), + ]) + featuresEntry.update({ badge: featuresBadge() }) + } + + ctx.rpc.register(action(RPC.refresh, refresh)) + ctx.rpc.register(action(RPC.resolve, async (params) => { + lastResult = runResolve(data, params?.id) + lastQuery = lastResult.id + await resolverUi.updateSpec(buildResolverSpec(data, lastQuery, lastResult)) + })) + ctx.rpc.register(action(RPC.clearLog, async () => { + data.resolution.clearRecords() + await resolverUi.updateSpec(buildResolverSpec(data, lastQuery, lastResult)) + })) + + ctx.commands.register({ + id: RPC.refresh, + title: 'vite-layers: Refresh panels', + icon: ICON.refresh, + handler: refresh, + } satisfies DevToolsServerCommandInput) + + void ctx.messages.add({ + id: `${NS}:ready`, + message: `vite-layers: ${snap.layers.length} layers, ${snap.featureLeafCount - snap.featureDisabledCount}/${snap.featureLeafCount} features enabled`, + level: 'info', + category: NS, + }) + }, + }, + } +} diff --git a/vite-layers/src/feature.ts b/vite-layers/src/feature.ts new file mode 100644 index 0000000..620a6c4 --- /dev/null +++ b/vite-layers/src/feature.ts @@ -0,0 +1,43 @@ +/** + * Public entry for the build-time feature macro. Import it as `#feature` (vite-layers + * auto-registers the alias and the tsconfig `paths` entry) or as `vite-layers/feature`: + * + * ```ts + * import { feature } from '#feature' + * + * const routes = [ + * { path: '/', component: () => import('@/pages/Home') }, + * feature('billing') && { path: '/billing', component: () => import('@/pages/Billing') }, + * ].filter(Boolean) + * ``` + * + * `feature('billing')` is replaced by the flag's literal value at compile time — **identically in + * dev and build** — so a disabled branch (and any `import()` inside it) is statically dead and is + * dropped from the bundle. The rules below are enforced: a violation fails the build (in dev and + * build alike), it never silently ships. + * + * - the argument must be a string literal: `feature('billing')`, never `feature(name)`; + * - call it directly — no aliasing (`const f = feature`), destructuring, or passing it as a value; + * - the key must exist in the merged `features` (a typo is also a TypeScript error). + * + * Nested flags are addressed with a dotted key: `feature('payments.stripe')`. + * + * This module has no runtime: every call is compiled away. The stub below only throws if a call + * survives — i.e. the vite-layers plugin did not run on this module. + */ + +/** Augmented by the generated `.vite-layers/features.d.ts` with the project's flags + literal types. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface LayerFeatures {} + +type FeatureKey = [keyof LayerFeatures] extends [never] ? string : keyof LayerFeatures + +export function feature( + key: K, +): K extends keyof LayerFeatures ? LayerFeatures[K] : unknown +export function feature(key: string): unknown { + throw new Error( + `vite-layers: feature(${JSON.stringify(key)}) was not compiled away. ` + + 'Make sure the vite-layers plugin is active and call feature() directly with a string-literal key.', + ) +} diff --git a/vite-layers/src/features.ts b/vite-layers/src/features.ts new file mode 100644 index 0000000..e6be50a --- /dev/null +++ b/vite-layers/src/features.ts @@ -0,0 +1,479 @@ +import MagicString from 'magic-string' +import { parseSync } from 'oxc-parser' +import type { Plugin } from 'vite' + +/** + * The reserved import specifier for the feature macro. vite-layers auto-registers this as a Vite + * alias (→ `src/feature.ts`) and a tsconfig `paths` entry, so consumers need no extra config. + * `vite-layers/feature` is also accepted (the published subpath export) for tooling that bypasses + * the alias. + */ +export const FEATURE_MODULE = '#feature' +const FEATURE_SPECIFIERS = new Set([FEATURE_MODULE, 'vite-layers/feature']) + +/** Matches an import/export `from '#feature'|'vite-layers/feature'` clause. Used to decide whether a + * parse failure must fail the build (the module really uses the macro) rather than be skipped. */ +const FEATURE_FROM_RE = /\bfrom\s*['"](?:#feature|vite-layers\/feature)['"]/ + +/** Code-filter regex for the rolldown `transform` hook filter (see {@link featurePlugin}). A superset + * of "actually imports the macro" — it just gates the JS round-trip; the handler decides precisely. */ +const MACRO_CODE_RE = /#feature|vite-layers\/feature/ + +// --------------------------------------------------------------------------------------------- +// Feature tree helpers (shared by the transform and the type generator so they never disagree). +// --------------------------------------------------------------------------------------------- + +/** + * Flatten a (possibly nested) feature object into every dotted path — both intermediate objects and + * leaves — paired with its value. `{ payments: { stripe: true } }` → + * `[['payments', {stripe:true}], ['payments.stripe', true]]`. The transform resolves a `feature()` + * key by exact lookup here, and the type generator emits one interface member per entry, so the + * accepted keys and the substituted values are guaranteed to match. + */ +export function flattenFeatures(features: Record): Array<[string, unknown]> { + const out: Array<[string, unknown]> = [] + const walk = (obj: Record, prefix: string) => { + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? `${prefix}.${k}` : k + out.push([key, v]) + if (v && typeof v === 'object' && !Array.isArray(v)) walk(v as Record, key) + } + } + walk(features, '') + return out +} + +const isPlainObject = (v: unknown): boolean => { + if (v === null || typeof v !== 'object' || Array.isArray(v)) return false + const proto = Object.getPrototypeOf(v) + return proto === Object.prototype || proto === null +} + +/** + * Returns a human description if `v` is NOT a JSON-like value the macro can fold into source (and + * the type generator into a literal type), else `null`. Recurses arrays/plain objects. Rejects + * bigint (JSON.stringify throws), functions/symbols (not serializable), non-finite numbers + * (`NaN`/`Infinity` → invalid TS + JSON `null`), and non-plain objects (Date/Map/RegExp/… would be + * coerced or crash). Keeping this strict means a bad flag fails fast with a clear message instead of + * crashing the transform or silently shipping a wrong value. + */ +function unsupportedValue(v: unknown): string | null { + if (v === null || v === undefined) return null + if (typeof v === 'boolean' || typeof v === 'string') return null + if (typeof v === 'number') return Number.isFinite(v) ? null : `non-finite number (${v})` + if (typeof v === 'bigint') return 'bigint' + if (typeof v === 'function') return 'function' + if (typeof v === 'symbol') return 'symbol' + if (Array.isArray(v)) { + for (const el of v) { + const bad = unsupportedValue(el) + if (bad) return bad + } + return null + } + if (isPlainObject(v)) { + for (const val of Object.values(v as Record)) { + const bad = unsupportedValue(val) + if (bad) return bad + } + return null + } + return `non-plain object (${Object.prototype.toString.call(v)})` +} + +/** + * Flatten + validate the feature tree. Throws (clear, fail-fast) on a dotted-key collision (an + * explicit `'a.b'` key clashing with a nested `a.b` path — they would produce a duplicate `.d.ts` + * member and an order-dependent wrong substitution) or an unsupported value type. Shared by the + * transform and the type generator so both reject the same inputs identically. + */ +export function validateFeatures(features: Record): Array<[string, unknown]> { + const flat = flattenFeatures(features) + const seen = new Set() + for (const [key, value] of flat) { + if (seen.has(key)) { + throw new Error( + `vite-layers: feature flag key '${key}' is defined twice — an explicit dotted key and a nested ` + + `path collide. Use one form, not both.`, + ) + } + seen.add(key) + const bad = unsupportedValue(value) + if (bad) { + throw new Error( + `vite-layers: feature flag '${key}' has an unsupported value type (${bad}). Flags must be ` + + `JSON-like: boolean, finite number, string, null, plain object, or array of those.`, + ) + } + } + return flat +} + +/** A property name that can be written unquoted in a TS type literal / object key. */ +const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/ +const tsKey = (k: string) => (IDENTIFIER_RE.test(k) ? k : JSON.stringify(k)) + +/** + * Render a value as a TS **literal** type (not the widened base type): `false`, `2`, `"app"`, + * `readonly [...]`, `{ … }`. Emitting the literal makes the macro's return type the exact value the + * transform substitutes, so editors show the real flag value and `keyof` typo-checks the key. + */ +function tsType(value: unknown): string { + if (value === null) return 'null' + if (value === undefined) return 'undefined' + if (Array.isArray(value)) { + return value.length ? `readonly [${value.map(tsType).join(', ')}]` : 'readonly []' + } + switch (typeof value) { + case 'boolean': + return value ? 'true' : 'false' + case 'number': + return String(value) + case 'string': + return JSON.stringify(value) + case 'object': { + const entries = Object.entries(value as Record) + if (entries.length === 0) return 'Record' + return `{ ${entries.map(([k, v]) => `${tsKey(k)}: ${tsType(v)}`).join('; ')} }` + } + default: + return 'unknown' + } +} + +/** + * Generate the `.d.ts` that augments {@link LayerFeatures} so `feature('key')` is typed with the + * flag's literal value and an unknown key (`feature('biling')`) is a compile error. The augmentation + * targets the `#feature` module, which vite-layers maps to `src/feature.ts` via the generated + * tsconfig `paths`. + */ +export function featuresDts(features: Record = {}): string { + const members = validateFeatures(features).map(([k, v]) => ` ${tsKey(k)}: ${tsType(v)}`) + return [ + '// AUTO-GENERATED by vite-layers — do not edit.', + `import '${FEATURE_MODULE}'`, + '', + `declare module '${FEATURE_MODULE}' {`, + ' interface LayerFeatures {', + ...members, + ' }', + '}', + '', + ].join('\n') +} + +// --------------------------------------------------------------------------------------------- +// The transform: replace `feature('key')` with a literal, fail the build on any other use. +// --------------------------------------------------------------------------------------------- + +/** `?…&lang.` query that Vue/Vite append to SFC sub-modules. Module-scope (not re-created per call). */ +const LANG_QUERY_RE = /[?&]lang\.(\w+)/ + +/** Pick the dialect for oxc from the module id (handles `.vue?…&lang.tsx` query ids). */ +function langFromId(id: string): 'js' | 'jsx' | 'ts' | 'tsx' { + const queryLang = id.match(LANG_QUERY_RE)?.[1] + const clean = id.split('?', 1)[0]! + const ext = queryLang ?? clean.slice(clean.lastIndexOf('.') + 1) + if (ext === 'tsx') return 'tsx' + if (ext === 'jsx') return 'jsx' + if (ext === 'js' || ext === 'mjs' || ext === 'cjs') return 'js' + // .ts/.mts/.cts and anything unknown → TS (a superset; the common case for app code). + return 'ts' +} + +type AnyNode = { type: string; start: number; end: number } & Record + +const CHILD_SKIP = new Set(['type', 'start', 'end', 'range', 'loc']) + +/** Iterate a node's child AST nodes (oxc emits an ESTree-shaped tree). */ +function eachChild(node: AnyNode, fn: (child: AnyNode) => void) { + for (const key in node) { + if (CHILD_SKIP.has(key)) continue + const child = node[key] + if (Array.isArray(child)) { + for (const c of child) if (c && typeof (c as AnyNode).type === 'string') fn(c as AnyNode) + } else if (child && typeof (child as AnyNode).type === 'string') { + fn(child as AnyNode) + } + } +} + +/** Collect the names bound by a binding pattern (Identifier / Object / Array / default / rest). */ +function patternNames(node: AnyNode | null | undefined, add: (name: string) => void): void { + if (!node || typeof node.type !== 'string') return + switch (node.type) { + case 'Identifier': + add(node.name as string) + break + case 'ObjectPattern': + for (const p of (node.properties as AnyNode[]) ?? []) { + patternNames((p.type === 'RestElement' ? p.argument : p.value) as AnyNode, add) + } + break + case 'ArrayPattern': + for (const el of (node.elements as (AnyNode | null)[]) ?? []) patternNames(el, add) + break + case 'AssignmentPattern': + patternNames(node.left as AnyNode, add) + break + case 'RestElement': + patternNames(node.argument as AnyNode, add) + break + } +} + +/** Lexical (block-scoped) bindings declared directly in a statement list: let/const/class/function. */ +function collectLexical(stmts: AnyNode[], add: (n: string) => void): void { + for (const st of stmts ?? []) { + if (st.type === 'VariableDeclaration' && st.kind !== 'var') { + for (const d of st.declarations as AnyNode[]) patternNames(d.id as AnyNode, add) + } else if ((st.type === 'FunctionDeclaration' || st.type === 'ClassDeclaration') && st.id) { + add((st.id as AnyNode).name as string) + } + } +} + +/** Function-scoped bindings hoisted in a body: `var` (at any depth) + nested function-decl names. */ +function collectHoisted(stmts: AnyNode[], add: (n: string) => void): void { + const visit = (node: AnyNode) => { + const t = node.type + if (t === 'FunctionDeclaration') { + if (node.id) add((node.id as AnyNode).name as string) + return // its body is a nested scope + } + if (t === 'FunctionExpression' || t === 'ArrowFunctionExpression' || t === 'ClassDeclaration' || t === 'ClassExpression') { + return // nested scope — its vars belong there + } + if (t === 'VariableDeclaration') { + if (node.kind === 'var') for (const d of node.declarations as AnyNode[]) patternNames(d.id as AnyNode, add) + return + } + eachChild(node, visit) + } + for (const s of stmts ?? []) visit(s) +} + +const SCOPE_NODES = new Set([ + 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression', + 'BlockStatement', 'StaticBlock', 'CatchClause', + 'ForStatement', 'ForInStatement', 'ForOfStatement', 'SwitchStatement', +]) + +/** The subset of `locals` (macro binding names) that this scope node re-binds, shadowing the import. */ +function scopeBindings(node: AnyNode, locals: Set): Set { + const bound = new Set() + const add = (n: string) => { + if (locals.has(n)) bound.add(n) + } + const t = node.type + if (t === 'FunctionDeclaration' || t === 'FunctionExpression' || t === 'ArrowFunctionExpression') { + for (const p of (node.params as AnyNode[]) ?? []) patternNames(p, add) + if (t === 'FunctionExpression' && node.id) add((node.id as AnyNode).name as string) + const body = node.body as AnyNode | undefined + if (body?.type === 'BlockStatement') collectHoisted(body.body as AnyNode[], add) + } else if (t === 'CatchClause') { + patternNames(node.param as AnyNode, add) + } else if (t === 'BlockStatement' || t === 'StaticBlock') { + collectLexical(node.body as AnyNode[], add) + } else if (t === 'ForStatement' || t === 'ForInStatement' || t === 'ForOfStatement') { + const head = (t === 'ForStatement' ? node.init : node.left) as AnyNode | null + if (head?.type === 'VariableDeclaration' && head.kind !== 'var') { + for (const d of head.declarations as AnyNode[]) patternNames(d.id as AnyNode, add) + } + } else if (t === 'SwitchStatement') { + for (const c of (node.cases as AnyNode[]) ?? []) collectLexical(c.consequent as AnyNode[], add) + } + return bound +} + +/** Extract a string key from a `feature(arg)` argument — a plain string literal or a `\`literal\``. */ +function stringKey(arg: AnyNode | undefined): string | undefined { + if (!arg) return undefined + if ((arg.type === 'Literal' || arg.type === 'StringLiteral') && typeof arg.value === 'string') { + return arg.value + } + if (arg.type === 'TemplateLiteral') { + const exprs = arg.expressions as unknown[] + const quasis = arg.quasis as Array<{ value: { cooked?: string } }> + // A single static chunk with a valid cooked value; an invalid escape (`\unicode`) makes cooked + // null → treat as not-a-string-literal so it routes to the clear "string-literal key" error. + if (exprs.length === 0 && quasis.length === 1 && typeof quasis[0]!.value.cooked === 'string') { + return quasis[0]!.value.cooked + } + } + return undefined +} + +/** A primitive substitutes bare; an object/array is parenthesized so it is always an expression. */ +function literalOf(value: unknown): string { + if (value === undefined) return 'undefined' + if (value !== null && typeof value === 'object') return `(${JSON.stringify(value)})` + return JSON.stringify(value) +} + +const isImportSource = (node: AnyNode, sources: Set): boolean => { + const src = node.source as { value?: unknown } | undefined + return typeof src?.value === 'string' && sources.has(src.value) +} + +/** + * Build-time feature flags via the `feature('key')` macro — one mechanism for dev **and** build. + * + * The transform parses every module that imports `feature` (from `#feature` / `vite-layers/feature`), + * replaces each `feature('key')` call with the flag's literal value, and removes the now-unused + * import. Replacing a disabled flag's call with `false` lets Rollup/rolldown tree-shake the dead + * branch — including any `import()` inside it — so the chunk is never emitted. + * + * Anything other than a direct call with a known string-literal key (aliasing, destructuring, + * dynamic key, unknown key) is a **hard error** via `this.error`, surfaced with a code frame in dev + * (browser overlay + terminal) and as a failed build — the misuse can never silently ship. Because + * the same substitution runs in dev, dev is a faithful oracle for the build result. + * + * @param features the merged feature flags (high→low layer priority already applied). + */ +export function featurePlugin(features: Record = {}): Plugin { + const flat = new Map(validateFeatures(features)) + + return { + name: 'vite-layers:features', + // `enforce: 'post'` so we always run *after* every framework/TS transform (a Vue SFC's + // `