feat: rename to v2dom

v3
veypi 1 month ago
parent 62711e1ff2
commit 9f789b78e7

@ -20,6 +20,10 @@ let cfg = proxy.Watch({
phone: '', phone: '',
icon: 'https://public.veypi.com/img/avatar/0001.jpg' icon: 'https://public.veypi.com/img/avatar/0001.jpg'
}, },
myapps: [
{ name: 'a' },
{ name: 'b' },
],
ready: false, ready: false,
local_user: {}, local_user: {},

@ -4,18 +4,18 @@
* 2024-10-22 22:07 * 2024-10-22 22:07
* Distributed under terms of the GPL license. * Distributed under terms of the GPL license.
*/ */
import b from './build' import v from './v2dom'
import cfg from '../cfg' import cfg from '../cfg'
export default b('voa-account', [ export default v('voa-account', [
b('voa-account-header', [b('voa-ah-1', '我的账户'), b('voa-ah-2')]), v('voa-account-header', [v('voa-ah-1', '我的账户'), v('voa-ah-2')]),
b('voa-account-body', [ v('voa-account-body', [
b('voa-ab-ico', [b('img', '', (d) => { d.setAttribute('src', cfg.user.icon) })]), v('voa-ab-ico', [v('img', '', (d) => { d.setAttribute('src', cfg.user.icon) })]),
b('voa-ab-info', [ v('voa-ab-info', [
b('voa-abi-1', [b('span', '昵称:'), b('span', '', (d) => { d.innerHTML = cfg.user.nickname })]), v('voa-abi-1', [v('span', '昵称:'), v('span', '', (d) => { d.innerHTML = cfg.user.nickname })]),
b('voa-abi-2', [b('span', '账户:'), b('span', '', (d) => { d.innerHTML = cfg.user.username })]), v('voa-abi-2', [v('span', '账户:'), v('span', '', (d) => { d.innerHTML = cfg.user.username })]),
b('voa-abi-3', [b('span', '邮箱:'), b('span', '', (d) => { d.innerHTML = cfg.user.email })]), v('voa-abi-3', [v('span', '邮箱:'), v('span', '', (d) => { d.innerHTML = cfg.user.email })]),
b('voa-abi-4', [b('span', '手机:'), b('span', '', (d) => { d.innerHTML = cfg.user.phone })]), v('voa-abi-4', [v('span', '手机:'), v('span', '', (d) => { d.innerHTML = cfg.user.phone })]),
]) ])
]) ])
]) ])

@ -5,20 +5,20 @@
* Distributed under terms of the GPL license. * Distributed under terms of the GPL license.
*/ */
import b from "./build"; import cfg from "../cfg";
import v from "./v2dom";
export default class { export default v({
main: HTMLElement
body: HTMLElement
constructor() {
this.body = b('voa-apps-body')
this.main = b({
class: 'voa-apps', class: 'voa-apps',
children: [ children: [
b({ class: 'voa-apps-header', children: [b('voa-apps-title')] }), v({ class: 'voa-apps-header', children: [v('voa-apps-title', '我的应用')] }),
this.body v('voa-apps-body', [
[cfg.myapps,
(data) => v('voa-app-box', '', (d) => d.innerHTML = data.name)],
v('div', '222'),
[cfg.myapps,
(data) => v('voa-app-box', '', (d) => d.innerHTML = data.name)],
])
] ]
}) })
}
}

@ -1,77 +0,0 @@
/*
* build.ts
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-10-23 15:54
* Distributed under terms of the GPL license.
*/
import proxy from "./proxy"
interface buildOpts {
id?: string
typ?: string
class?: string
style?: string
innerHtml?: string
onclick?: any
children?: HTMLElement[]
updator?: (p: HTMLElement) => void
}
const typs = ['div', 'img', 'span', 'p', 'a']
export default (opts: buildOpts | string, inner?: string | HTMLElement[], updator?: (p: HTMLElement) => void) => {
if (typeof opts === 'string') {
if (typs.indexOf(opts) >= 0) {
opts = { typ: opts }
} else {
opts = { class: opts }
}
}
if (inner) {
if (typeof inner == 'string') {
opts.innerHtml = inner
} else {
opts.children = inner
}
}
if (opts.updator) {
updator = opts.updator
}
let dom = document.createElement(opts.typ || 'div')
if (opts.id) {
dom.id = opts.id
}
if (opts.class) {
dom.classList.add(...opts.class.split(' '))
}
if (opts.innerHtml) {
dom.innerHTML = opts.innerHtml
}
if (opts.onclick) {
dom.onclick = opts.onclick
}
if (opts.children) {
for (let c in opts.children) {
dom.appendChild(opts.children[c])
}
}
if (opts.style) {
const regex = /([a-zA-Z-]+)\s*:\s*([^;]+);?/g;
let match: any
while ((match = regex.exec(opts.style)) !== null) {
const key = match[1].trim();
const value = match[2].trim();
dom.style.setProperty(key, value)
}
}
dom.setAttribute('voa', '1')
if (updator) {
proxy.Listen(() => {
updator(dom)
})
}
return dom
}

@ -6,7 +6,7 @@
*/ */
import slide from './slide' import slide from './slide'
import b from './build' import v from './v2dom'
import bus from '../bus' import bus from '../bus'
export default class { export default class {
slide: slide slide: slide
@ -24,7 +24,7 @@ export default class {
}) })
} }
mount_login() { mount_login() {
this.frame_login = b({ this.frame_login = v({
class: 'voa-off voa-hover-line-b voa-scale-in', class: 'voa-off voa-hover-line-b voa-scale-in',
innerHtml: '登录', innerHtml: '登录',
onclick: () => { onclick: () => {
@ -40,7 +40,7 @@ export default class {
} }
mount_user() { mount_user() {
let icon = 'https://public.veypi.com/img/avatar/0001.jpg' let icon = 'https://public.veypi.com/img/avatar/0001.jpg'
this.frame_user = b({ this.frame_user = v({
class: 'voa-on voa-scale-in', class: 'voa-on voa-scale-in',
innerHtml: ` innerHtml: `
<img src="${icon}" /> <img src="${icon}" />

@ -6,8 +6,22 @@
*/ */
type voidFn = () => void type voidFn = () => void
// TODO: 没有删除机制
const callbackCache: voidFn[] = [] const callbackCache: voidFn[] = []
const cacheUpdateList: number[] = []
// 界面响应频率40hz
setInterval(() => {
let list = new Set(cacheUpdateList.splice(0))
for (let l of list) {
callbackCache[l]()
}
}, 25)
function generateUniqueId() {
const timestamp = performance.now().toString(36);
const random = Math.random().toString(36).substring(2, 5);
return `${timestamp}-${random}`;
}
function ForceUpdate() { function ForceUpdate() {
for (let c of callbackCache) { for (let c of callbackCache) {
@ -15,51 +29,77 @@ function ForceUpdate() {
} }
} }
var listen_tag = -1 var listen_tags: number[] = []
function Listen(callback: voidFn) { function Listen(callback: voidFn) {
if (listen_tag >= 0) { listen_tags.push(callbackCache.length)
console.warn('it shuold not happen')
return
}
listen_tag = callbackCache.length
callbackCache.push(callback) callbackCache.push(callback)
callback() callback()
listen_tag = -1 listen_tags.pop()
} }
const isProxy = Symbol("isProxy")
const DataID = Symbol("DataID")
function Watch<T extends Object>(target: T) { function Watch<T extends Object>(data: T) {
const listeners: { [key: string]: number[] } = {} const did = generateUniqueId()
let isArray = false
if (Object.prototype.toString.call(data) === '[object Array]') {
isArray = true
}
// console.log(`watch ${did} ${isArray}`, data)
const listeners: { [key: string | symbol]: number[] } = {}
const handler = { const handler = {
get(target: Object, key: string, receiver: any) { get(target: Object, key: string | symbol, receiver: any) {
console.log(`get ${key} ${listen_tag}`) if (key === isProxy) {
const value = Reflect.get(target, key, receiver); return true
}
if (key === DataID) {
return did
}
const value = Reflect.get(target, key, receiver)
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
return new Proxy(value, handler); if (value[isProxy]) {
return value
} else {
let newValue = Watch(value)
Reflect.set(target, key, newValue, receiver)
return newValue
}
} }
if (listen_tag >= 0) { let idx = -1
if (!listeners[key]) { if (listen_tags.length > 0) {
listeners[key] = [] let lkey = key
idx = listen_tags[listen_tags.length - 1]
if (isArray) {
lkey = ''
} }
listeners[key].push(listen_tag) if (!listeners[lkey]) {
listeners[lkey] = [idx]
} else if (listeners[lkey].indexOf(idx) == -1) {
listeners[lkey].push(idx)
} }
}
// console.log(`${did} get ${key.toString()}:${value} ${idx}`)
return value; return value;
}, },
set(target: Object, key: string, newValue: any, receiver: any) { set(target: Object, key: string | symbol, newValue: any, receiver: any) {
console.log(`set ${key} ${newValue}`) // console.log(`${did} set ${key.toString()} ${newValue}`)
const result = Reflect.set(target, key, newValue, receiver); const result = Reflect.set(target, key, newValue, receiver);
if (result) { if (result) {
if (listeners[key]) { let lkey = key
console.log(listeners[key]) if (isArray) {
for (let cb of listeners[key]) { lkey = ''
callbackCache[cb]() }
if (listeners[lkey]) {
for (let cb of listeners[lkey]) {
cacheUpdateList.push(cb)
} }
} }
} }
return result; return result;
}, },
deleteProperty(target: Object, key: string) { deleteProperty(target: Object, key: string) {
console.log(`del ${key}`) // console.log(`del ${key}`)
const result = Reflect.deleteProperty(target, key); const result = Reflect.deleteProperty(target, key);
if (result) { if (result) {
} }
@ -67,7 +107,10 @@ function Watch<T extends Object>(target: T) {
} }
}; };
return new Proxy<T>(target, handler); let res = new Proxy<T>(data, handler);
// Symbol(Symbol.toStringTag)
// res[Symbol.toStringTag] = 'Proxy'
return res
} }
export default { Watch, Listen, ForceUpdate } export default { Watch, Listen, ForceUpdate, DataID, generateUniqueId }

@ -4,10 +4,10 @@
* 2024-10-22 17:57 * 2024-10-22 17:57
* Distributed under terms of the GPL license. * Distributed under terms of the GPL license.
*/ */
import b from './build' import v from './v2dom'
import bus from '../bus' import bus from '../bus'
import account from './account' import account from './account'
import cfg from '../cfg' import app from './app'
/* /*
mask mask
@ -26,38 +26,36 @@ export default class {
main: HTMLElement main: HTMLElement
footer: HTMLElement footer: HTMLElement
constructor() { constructor() {
this.header = b({ this.header = v({
class: 'voa-slide-header voa-animate-slow', class: 'voa-slide-header voa-animate-slow',
}) })
this.footer = b({ this.footer = v({
class: 'voa-slide-footer', class: 'voa-slide-footer',
innerHtml: 'logout', innerHtml: 'logout',
onclick: () => { onclick: () => {
bus.emit('logout') bus.emit('logout')
} }
}) })
this.main = b({ this.main = v({
class: 'voa-slide-main', class: 'voa-slide-main',
children: [ children: [
account, account,
b({ class: 'voa-sm-separate' }) v({ class: 'voa-sm-separate' }),
], app,
onclick: () => { v({ class: 'voa-sm-separate' })
console.log(cfg) ]
cfg.user.phone = new Date().toLocaleString()
}
}) })
this.body = b({ this.body = v({
class: 'voa-slide-body voa-animate-slow', class: 'voa-slide-body voa-animate-slow',
style: 'animation-delay: 300ms', style: 'animation-delay: 300ms',
children: [this.main, this.footer] children: [this.main, this.footer]
}) })
this.slide = b({ this.slide = v({
id: 'voa-slide', id: 'voa-slide',
class: 'voa-slide', class: 'voa-slide',
children: [this.header, this.body] children: [this.header, this.body]
}) })
this.mask = b({ this.mask = v({
class: 'voa-slide-mask', class: 'voa-slide-mask',
style: 'visibility: hidden', style: 'visibility: hidden',
children: [this.slide], children: [this.slide],

@ -0,0 +1,154 @@
/*
* v2dom.ts
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-10-23 15:54
* Distributed under terms of the GPL license.
*/
import proxy from "./proxy"
interface buildOpts<T> {
id?: string
typ?: string
class?: string
style?: string
innerHtml?: string
onclick?: any
children?: childTyp<T>[]
updator?: (p: HTMLElement) => void
}
const typs = ['div', 'img', 'span', 'p', 'a']
type iterChild<T> = [T[], (d: T) => HTMLElement]
type childTyp<T> = HTMLElement | iterChild<T>
export default <T>(opts: buildOpts<T> | string, inner?: string | childTyp<T>[], updator?: (p: HTMLElement) => void) => {
if (typeof opts === 'string') {
if (typs.indexOf(opts) >= 0) {
opts = { typ: opts }
} else {
opts = { class: opts }
}
}
if (inner) {
if (typeof inner == 'string') {
opts.innerHtml = inner
} else {
opts.children = inner
}
}
if (opts.updator) {
updator = opts.updator
}
let dom = document.createElement(opts.typ || 'div')
if (opts.id) {
dom.id = opts.id
}
if (opts.class) {
dom.classList.add(...opts.class.split(' '))
}
if (opts.innerHtml) {
dom.innerHTML = opts.innerHtml
}
if (opts.onclick) {
dom.onclick = opts.onclick
}
if (opts.children) {
let tmpID = ''
for (let c of opts.children) {
if (Object.prototype.toString.call(c) === '[object Array]') {
const iterID = proxy.generateUniqueId()
const iterLast = tmpID
proxy.Listen(() => {
c = c as iterChild<T>
let itemIDs: string[] = []
for (let i = 0; i < c[0].length; i++) {
let line = c[0][i]
//@ts-ignore
let itemID = line[proxy.DataID] || ''
let nextID = ''
if (i < c[0].length - 1) {
//@ts-ignore
nextID = c[0][i + 1][proxy.DataID] || ''
}
itemIDs.push(itemID)
let inserted = -2
let subdom: HTMLElement | null = null
let beforeChild: HTMLElement | null = null
for (let childID = 0; childID < dom.children.length; childID++) {
let child = dom.children[childID]
// console.log(`${inserted} ${childID} ${itemID} ${nextID}`)
if (child.getAttribute('vbind-iter') === iterID) {
if (child.getAttribute('vbind-proxy') === itemID) {
inserted = childID
subdom = child as HTMLElement
} else if (child.getAttribute('vbind-proxy') === nextID && nextID != '') {
// 用于交换位置
if (inserted != childID - 1) {
beforeChild = child as HTMLElement
}
}
} else if (child.getAttribute('vbind-iterlast') === iterID) {
if (inserted == -2) {
beforeChild = child as HTMLElement
}
// 找到第一个不是这个循环块的dom元素时退出
break
}
}
if (inserted == -2) {
subdom = c[1](line)
subdom.setAttribute('vbind-proxy', itemID)
subdom.setAttribute('vbind-iter', iterID)
if (iterLast != '') {
subdom.setAttribute('vbind-iterlast', iterLast)
}
if (beforeChild) {
dom.insertBefore(subdom, beforeChild)
// console.log(`${iterID} insert new`, subdom)
} else {
// console.log(`${iterID} append new`, subdom)
dom.appendChild(subdom)
}
} else if (beforeChild) {
// console.log(`${iterID} insert old`, subdom)
dom.insertBefore(subdom!, beforeChild)
} else {
// console.log(`${iterID} none`, subdom)
}
}
for (let child of dom.children) {
if (child.getAttribute('vbind-iter') === iterID && itemIDs.indexOf(child.getAttribute('vbind-proxy') || '') == -1) {
dom.removeChild(child)
}
}
})
tmpID = iterID
} else {
c = c as HTMLElement
if (tmpID != '') {
c.setAttribute('vbind-iterlast', tmpID)
}
dom.appendChild(c)
}
}
}
if (opts.style) {
const regex = /([a-zA-Z-]+)\s*:\s*([^;]+);?/g;
let match: any
while ((match = regex.exec(opts.style)) !== null) {
const key = match[1].trim();
const value = match[2].trim();
dom.style.setProperty(key, value)
}
}
dom.setAttribute('voa', '1')
if (updator) {
proxy.Listen(() => {
updator(dom)
})
}
return dom
}

@ -7,7 +7,6 @@
import './assets/css/oaer.scss' import './assets/css/oaer.scss'
import bus from './bus' import bus from './bus'
import ui from './components' import ui from './components'
export class OAer { export class OAer {

@ -22,7 +22,8 @@
"devDependencies": { "devDependencies": {
"sass": "^1.77.6", "sass": "^1.77.6",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vite": "^5.2.10" "vite": "^5.2.10",
"vite-plugin-css-injected-by-js": "^3.5.2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

@ -1,6 +1,8 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
export default defineConfig({ export default defineConfig({
plugins: [cssInjectedByJsPlugin()],
build: { build: {
lib: { lib: {
entry: './lib/main.ts', entry: './lib/main.ts',

@ -229,7 +229,7 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
braces@~3.0.2: braces@^3.0.3, braces@~3.0.2:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
@ -354,6 +354,14 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
micromatch@^4.0.7:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0: mime-db@1.52.0:
version "1.52.0" version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
@ -386,7 +394,7 @@ picocolors@^1.0.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
picomatch@^2.0.4, picomatch@^2.2.1: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@ -463,6 +471,18 @@ typescript@^5.4.5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa"
integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==
vite-plugin-css-injected-by-js@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz#1f75d16ad5c05b6b49bf18018099a189ec2e46ad"
integrity sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==
vite-plugin-singlefile@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/vite-plugin-singlefile/-/vite-plugin-singlefile-2.0.2.tgz#af2c95e0182bd363dbe29b80bea5c5a78209f649"
integrity sha512-Z2ou6HcvED5CF0hM+vcFSaFa+klyS8RyyLxW0PbMRLnMbvzTI6ueWyxdYNFhpuXZgz/aj6+E/dHFTdEcw6gb9w==
dependencies:
micromatch "^4.0.7"
vite@^5.2.10: vite@^5.2.10:
version "5.3.3" version "5.3.3"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.3.tgz#5265b1f0a825b3b6564c2d07524777c83e3c04c2" resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.3.tgz#5265b1f0a825b3b6564c2d07524777c83e3c04c2"

Loading…
Cancel
Save