Add CUTOS capability and core API examples

This commit is contained in:
yankun
2026-07-26 14:40:42 +08:00
commit 7cc505a5e2
61 changed files with 5136 additions and 0 deletions
@@ -0,0 +1 @@
VITE_CUTOS_BROKER_URL=mock
@@ -0,0 +1 @@
VITE_CUTOS_BROKER_URL=localhost
@@ -0,0 +1,7 @@
node_modules
dist
dist-ssr
release
*.local
*.log
.DS_Store
@@ -0,0 +1,10 @@
{
"printWidth": 200,
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"trailingComma": "none",
"plugins": ["prettier-plugin-tailwindcss"]
}
@@ -0,0 +1,14 @@
# CUTOS Receipt Printer Demo
End-to-end LWA for `@cutos/device-receipt-printer` and the MS-MA90 Provider.
The Demo covers USB/serial connection, device information, styled text, QR code, paper feed, cutting, status, and Device events. Text and QR printing explicitly call `feed({ dots: 240 })` after printing so the result advances to the paper outlet. The standalone Feed action also defaults to 240 dots. Development uses CUTOS Mock through `.env.development`; deployed builds use CUTOS Runtime and the declared Provider dependency.
The card grid is responsive: portrait displays use two columns, landscape displays use four columns, and narrow displays use one column.
```sh
npm install
npx cutos lwa build
npx cutos lwa upload
npx cutos lwa publish --device <device-id>
```
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CUTOS LWA</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
{
"name": "demo-receipt-printer",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "cutos lwa build",
"build:app": "vue-tsc --noEmit && vite build",
"package": "cutos lwa package",
"validate": "cutos lwa validate",
"preview": "vite preview"
},
"dependencies": {
"@cutos/core": "^4.0.8",
"@cutos/device-receipt-printer": "^4.0.0",
"vue": "^3.4.31"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.40",
"postcss-import": "^16.1.0",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.4",
"unplugin-auto-import": "^0.18.0",
"vite": "^5.3.4",
"vue-tsc": "^2.0.26"
}
}
@@ -0,0 +1,7 @@
export default {
plugins: {
'postcss-import': {},
tailwindcss: {},
autoprefixer: {}
}
}
@@ -0,0 +1,12 @@
{
"name": "demo-receipt-printer",
"version": "0.1.3",
"description": "CUTOS 4.0 Receipt Printer end-to-end demo",
"params": {
"title": "CUTOS Receipt Printer",
"host": "localhost"
},
"drvDependencies": {
"device-receipt-printer-provider-ms-ma90": "0.1.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,140 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { CoreAPI } from '@cutos/core'
import {
DeviceCapabilityReceiptPrinter,
type DeviceInfo,
type GetStatusResult
} from '@cutos/device-receipt-printer'
import { loadConfig } from '@/utils/config'
import cutosLogo from '@/assets/cutos.png'
type EventKind = 'data' | 'status' | 'error'
type Alignment = 'left' | 'center' | 'right'
interface EventItem { id: number; time: string; kind: EventKind; value: unknown }
interface ReceiptPrinterClient {
init(): Promise<unknown>
connect(params?: { connection?: 'usb' | 'serial'; port?: string; baudRate?: 9600 | 38400 | 115200 }): Promise<{ connected: boolean }>
disconnect(): Promise<{ connected: boolean }>
readDeviceInfo(): Promise<DeviceInfo>
printText(params: { text: string; alignment?: Alignment; width?: number; height?: number; bold?: boolean }): Promise<{ success: boolean }>
printQrCode(params: { content: string; size?: number; leftMargin?: number }): Promise<{ success: boolean }>
feed(params?: { dots?: number }): Promise<{ success: boolean }>
cut(params?: { mode?: 'full' | 'partial' }): Promise<{ success: boolean }>
getStatus(): Promise<GetStatusResult>
onData(fn: (value: unknown) => void): () => void
onStatus(fn: (value: unknown) => void): () => void
onError(fn: (value: unknown) => void): () => void
dispose(): boolean
}
class MockReceiptPrinter implements ReceiptPrinterClient {
private listeners = { data: new Set<(v: any) => void>(), status: new Set<(v: any) => void>(), error: new Set<(v: any) => void>() }
async init() { return 'Mock receipt printer initialized' }
async connect() { this.emit('status', { status: 'online', updatedAt: Date.now() }); return { connected: true } }
async disconnect() { this.emit('status', { status: 'offline', updatedAt: Date.now() }); return { connected: false } }
async readDeviceInfo() { return { manufacturer: 'MASUNG', model: 'MS-MA90 Mock', version: '0.1.0', deviceType: 'receipt-printer', sdkVersion: '2.2.2.10' } }
async printText() { return this.complete('printText') }
async printQrCode() { return this.complete('printQrCode') }
async feed() { return this.complete('feed') }
async cut() { return this.complete('cut') }
async getStatus() { return { code: 0, state: 'ready' as const, ready: true, message: 'Printer is ready.' } }
onData(fn: (v: unknown) => void) { return this.listen('data', fn) }
onStatus(fn: (v: unknown) => void) { return this.listen('status', fn) }
onError(fn: (v: unknown) => void) { return this.listen('error', fn) }
dispose() { Object.values(this.listeners).forEach(value => value.clear()); return true }
private async complete(operation: string) { this.emit('data', { type: 'operation-complete', operation, success: true, completedAt: Date.now() }); return { success: true } }
private listen(kind: EventKind, fn: (v: any) => void) { this.listeners[kind].add(fn); return () => this.listeners[kind].delete(fn) }
private emit(kind: EventKind, value: unknown) { this.listeners[kind].forEach(fn => fn(value)) }
}
const title = ref('CUTOS Receipt Printer')
const host = ref('localhost')
const coreVersion = ref('')
const lwaName = ref('')
const lwaVersion = ref('')
const runtimeConnected = ref(false)
const initialized = ref(false)
const connected = ref(false)
const busy = ref('')
const errorText = ref('')
const deviceInfo = ref<DeviceInfo | null>(null)
const result = ref<unknown>(null)
const printerStatus = ref<GetStatusResult | null>(null)
const connection = ref<'usb' | 'serial'>('usb')
const port = ref('')
const baudRate = ref<9600 | 38400 | 115200>(115200)
const text = ref('CUTOS 4.0\nMS-MA90 receipt printer')
const alignment = ref<Alignment>('left')
const bold = ref(false)
const width = ref(1)
const height = ref(1)
const qrContent = ref('https://www.cut-os.com')
const qrSize = ref(6)
const qrLeftMargin = ref(0)
const feedDots = ref(240)
const cutMode = ref<'full' | 'partial'>('partial')
const events = ref<EventItem[]>([])
const dataEvents = computed(() => events.value.filter(event => event.kind === 'data'))
const statusEvents = computed(() => events.value.filter(event => event.kind === 'status'))
const errorEvents = computed(() => events.value.filter(event => event.kind === 'error'))
let device: ReceiptPrinterClient | null = null
let unsubscribers: Array<() => void> = []
function format(value: unknown) { return value == null ? 'No data' : typeof value === 'string' ? value : JSON.stringify(value, null, 2) }
function addEvent(kind: EventKind, value: unknown) { events.value.unshift({ id: Date.now() + Math.random(), time: new Date().toLocaleTimeString(), kind, value }); events.value = events.value.slice(0, 40) }
async function run(name: string, action: () => Promise<void>) { busy.value = name; errorText.value = ''; try { await action() } catch (error) { errorText.value = error instanceof Error ? error.message : String(error); addEvent('error', errorText.value) } finally { busy.value = '' } }
async function connect() { await run('connect', async () => { const params = connection.value === 'serial' ? { connection: connection.value, port: port.value, baudRate: baudRate.value } : { connection: connection.value, ...(port.value ? { port: port.value } : {}) }; connected.value = (await device!.connect(params)).connected; if (connected.value) { deviceInfo.value = await device!.readDeviceInfo(); printerStatus.value = await device!.getStatus() } }) }
async function disconnect() { await run('disconnect', async () => { await device!.disconnect(); connected.value = false }) }
async function printText() { await run('printText', async () => { const printed = await device!.printText({ text: text.value, alignment: alignment.value, bold: bold.value, width: width.value, height: height.value }); const fed = await device!.feed({ dots: 240 }); result.value = { printed, fed, feedDots: 240 } }) }
async function printQrCode() { await run('printQrCode', async () => { const printed = await device!.printQrCode({ content: qrContent.value, size: qrSize.value, leftMargin: qrLeftMargin.value }); const fed = await device!.feed({ dots: 240 }); result.value = { printed, fed, feedDots: 240 } }) }
async function feed() { await run('feed', async () => { result.value = await device!.feed({ dots: feedDots.value }) }) }
async function cut() { await run('cut', async () => { result.value = await device!.cut({ mode: cutMode.value }) }) }
async function refreshStatus() { await run('getStatus', async () => { printerStatus.value = await device!.getStatus() }) }
onMounted(async () => {
try {
const config = await loadConfig()
title.value = String(config.params.title || title.value)
lwaName.value = config.name
lwaVersion.value = config.version
host.value = import.meta.env.VITE_CUTOS_BROKER_URL || String(config.params.host || 'localhost')
await CoreAPI.init(host.value)
runtimeConnected.value = CoreAPI.connected()
coreVersion.value = CoreAPI.getVersion()
device = host.value === 'mock' ? new MockReceiptPrinter() : new DeviceCapabilityReceiptPrinter()
await device.init()
unsubscribers = [
device.onData(value => addEvent('data', value)),
device.onStatus(value => { addEvent('status', value); const state = value && typeof value === 'object' && 'status' in value ? value.status : ''; if (state === 'online') connected.value = true; if (state === 'offline') connected.value = false }),
device.onError(value => addEvent('error', value))
]
initialized.value = true
} catch (error) { errorText.value = error instanceof Error ? error.message : String(error) }
})
onUnmounted(() => { unsubscribers.forEach(unsubscribe => unsubscribe()); device?.dispose() })
</script>
<template>
<div class="app-shell">
<header class="topbar">
<div class="brand"><img :src="cutosLogo" alt="CUTOS"><div><h1>{{ title }}</h1><p>Device Capability · CUTOS 4.0</p></div></div>
<div class="status-cluster"><span class="version-tag">Core {{ coreVersion || '—' }}</span><span class="version-tag">LWA {{ lwaName }} {{ lwaVersion || '—' }}</span><span class="connection" :class="{ online: runtimeConnected }"><i></i>{{ runtimeConnected ? 'Connected' : 'Disconnected' }}</span></div>
</header>
<main>
<section v-if="errorText" class="error-banner"><strong>Operation failed</strong><span>{{ errorText }}</span></section>
<section class="section-heading"><div><span class="eyebrow">{{ host }}</span><h2>Receipt Printer · MS-MA90</h2></div><div class="dependency-tags"><span class="provider-tag">Capability: <strong>@cutos/device-receipt-printer 4.0.0</strong></span><span class="provider-tag">Provider: <strong>device-receipt-printer-provider-ms-ma90 0.1.0</strong></span></div></section>
<section class="result-grid">
<article class="result-card"><div class="result-header method-header"><div><h3>Connection</h3><code>connect() / disconnect()</code></div><span class="state-tag" :class="{ online: connected }">{{ connected ? 'online' : 'offline' }}</span></div><div class="card-body form"><label>Connection<select v-model="connection"><option value="usb">USB</option><option value="serial">Serial</option></select></label><label>{{ connection === 'usb' ? 'USB port (optional)' : 'COM port' }}<input v-model="port" :placeholder="connection === 'usb' ? 'Auto detect' : 'COM1'"></label><label v-if="connection === 'serial'">Baud rate<select v-model="baudRate"><option :value="9600">9600</option><option :value="38400">38400</option><option :value="115200">115200</option></select></label><div class="button-row"><button class="button primary" :disabled="!initialized || !!busy || connected" @click="connect">Connect</button><button class="button secondary" :disabled="!connected || !!busy" @click="disconnect">Disconnect</button></div></div></article>
<article class="result-card"><div class="result-header"><h3>Device information</h3><code>readDeviceInfo()</code></div><pre>{{ format(deviceInfo) }}</pre></article>
<article class="result-card"><div class="result-header"><h3>Text</h3><code>printText()</code></div><div class="card-body form"><label>Text<textarea v-model="text" rows="3"></textarea></label><div class="inline-fields"><label>Align<select v-model="alignment"><option>left</option><option>center</option><option>right</option></select></label><label>Width<input v-model.number="width" type="number" min="1" max="8"></label><label>Height<input v-model.number="height" type="number" min="1" max="8"></label></div><label class="check"><input v-model="bold" type="checkbox">Bold</label><button class="button primary" :disabled="!connected || !!busy || !text" @click="printText">Print text</button></div></article>
<article class="result-card"><div class="result-header"><h3>QR code</h3><code>printQrCode()</code></div><div class="card-body form"><label>Content<input v-model="qrContent"></label><div class="inline-fields"><label>Size<input v-model.number="qrSize" type="number" min="1" max="8"></label><label>Left margin (mm)<input v-model.number="qrLeftMargin" type="number" min="0" max="27"></label></div><button class="button primary" :disabled="!connected || !!busy || !qrContent" @click="printQrCode">Print QR code</button></div></article>
<article class="result-card"><div class="result-header"><h3>Paper</h3><code>feed() / cut()</code></div><div class="card-body form"><label>Feed dots<input v-model.number="feedDots" type="number" min="0" max="250"></label><button class="button secondary" :disabled="!connected || !!busy" @click="feed">Feed paper</button><label>Cut mode<select v-model="cutMode"><option value="partial">Partial</option><option value="full">Full</option></select></label><button class="button primary" :disabled="!connected || !!busy" @click="cut">Cut paper</button></div></article>
<article class="result-card"><div class="result-header"><h3>Printer status</h3><code>getStatus()</code></div><div class="card-body"><button class="button secondary" :disabled="!connected || !!busy" @click="refreshStatus">Refresh</button><pre>{{ format(printerStatus) }}</pre></div></article>
<article class="result-card event-card"><div class="result-header"><h3>Operations</h3><code>onData()</code></div><div class="event-list"><div v-for="event in dataEvents" :key="event.id" class="event-row"><time>{{ event.time }}</time><pre>{{ format(event.value) }}</pre></div><p v-if="!dataEvents.length" class="empty-state">Waiting for operations</p></div></article>
<article class="result-card event-card"><div class="result-header"><h3>Status / Errors</h3><code>onStatus() / onError()</code></div><div class="event-list"><div v-for="event in [...statusEvents, ...errorEvents]" :key="event.id" class="event-row" :class="{ 'error-event': event.kind === 'error' }"><time>{{ event.time }}</time><pre>{{ format(event.value) }}</pre></div><p v-if="!statusEvents.length && !errorEvents.length" class="empty-state">Waiting for status</p></div></article>
</section>
<section class="last-result"><strong>Last result</strong><pre>{{ format(result) }}</pre></section>
</main>
</div>
</template>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
@@ -0,0 +1,10 @@
@tailwind base; @tailwind components; @tailwind utilities;
:root{font-family:Inter,"Segoe UI",Arial,sans-serif;color:#20262c;background:#eef1f3;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.48}.app-shell{min-height:100vh;background:#eef1f3}
.topbar{min-height:82px;padding:14px clamp(18px,4vw,52px);display:flex;align-items:center;justify-content:space-between;gap:24px;color:#f8fafb;background:#20262c;border-bottom:4px solid #16a394}.brand{display:flex;align-items:center;gap:15px}.brand img{width:42px;height:42px;object-fit:contain}.brand h1{margin:0;font-size:20px}.brand p{margin:4px 0 0;font-size:12px;color:#aeb8bf}.status-cluster,.dependency-tags,.button-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.version-tag,.connection{height:30px;display:inline-flex;align-items:center;padding:0 10px;font-size:12px;white-space:nowrap;border:1px solid #4b555d;border-radius:4px;background:#2b3339}.connection i{width:8px;height:8px;margin-right:7px;border-radius:50%;background:#e06b65}.connection.online i{background:#42c6a8}
main{width:100%;padding:28px clamp(18px,2vw,36px) 32px}.error-banner{display:flex;gap:12px;margin-bottom:20px;padding:12px 16px;color:#7e2420;background:#fff0ef;border-left:4px solid #d54d47}.section-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:15px}.section-heading h2{margin:3px 0 0;font-size:18px}.eyebrow{color:#64717a;font:11px "Cascadia Code",Consolas,monospace}.dependency-tags{justify-content:flex-end}.provider-tag{padding:6px 9px;color:#5d6870;background:#f8fafb;border:1px solid #cbd3d8;border-radius:4px;font:11px "Cascadia Code",Consolas,monospace}.provider-tag strong{color:#253038}
.result-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.result-card{min-width:0;min-height:225px;background:#fff;border:1px solid #d8dee2;border-radius:6px;overflow:hidden}.result-header{min-height:66px;padding:13px 15px;border-bottom:1px solid #e4e8eb;background:#f8fafb}.result-header h3{margin:0 0 6px;font-size:14px}.result-header code{display:block;color:#68757d;font-size:11px;overflow-wrap:anywhere}.method-header{display:flex;align-items:center;justify-content:space-between;gap:14px}.state-tag{padding:5px 8px;color:#8b3c37;background:#fff0ef;border-radius:3px;font:10px "Cascadia Code",Consolas,monospace}.state-tag.online{color:#087b71;background:#e8f7f4}.card-body{padding:15px}.button{min-height:34px;padding:7px 13px;border:1px solid transparent;border-radius:4px;font-weight:650;font-size:12px}.button.primary{color:#fff;background:#138b80;border-color:#138b80}.button.secondary{color:#2d373d;background:#fff;border-color:#bfc8cd}
pre{margin:0;padding:15px;font:12px/1.55 "Cascadia Code",Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere;overflow:auto}.result-card>pre{height:210px}.card-body>pre{height:110px;margin-top:12px;padding:10px;background:#f7f9fa;border:1px solid #e0e5e8}.form{display:grid;gap:12px}.form label{display:grid;gap:6px;color:#5d6870;font-size:12px}.form input,.form select,.form textarea{width:100%;min-height:36px;padding:7px 9px;border:1px solid #bfc8cd;border-radius:4px;background:#fff}.form textarea{resize:vertical}.form .check{display:flex;align-items:center;grid-template-columns:auto 1fr}.form .check input{width:auto;min-height:auto}.inline-fields{display:grid;grid-template-columns:2fr 1fr 1fr;gap:8px}.last-result{display:grid;grid-template-columns:auto minmax(0,1fr);gap:15px;align-items:start;margin-top:12px;padding:12px 15px;background:#fff;border:1px solid #d8dee2;border-radius:6px}.last-result strong{padding-top:4px;font-size:12px}.last-result pre{padding:4px 0}
.event-card{height:360px;display:grid;grid-template-rows:auto minmax(0,1fr)}.event-list{min-height:0;overflow:auto}.event-row{display:grid;grid-template-columns:78px minmax(0,1fr);gap:10px;padding:8px 13px;border-bottom:1px solid #edf0f2}.event-row time{padding-top:4px;color:#77838b;font-size:11px}.event-row pre{padding:4px 0}.error-event pre{color:#b43b35}.empty-state{margin:0;padding:24px 15px;color:#77838b;font-size:12px}
@media(orientation:landscape) and (min-width:1000px){.result-grid{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media(max-width:640px){.topbar,.section-heading{align-items:flex-start;flex-direction:column}.result-grid{grid-template-columns:1fr}.inline-fields{grid-template-columns:1fr}.last-result{grid-template-columns:1fr}}
.file-name{color:#77838b;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -0,0 +1,66 @@
export interface LwaConfig {
name: string
version: string
description?: string
params: {
title: string
host: string
[key: string]: unknown
}
drvDependencies?: Record<string, string>
}
const defaultConfig: LwaConfig = {
name: 'cutos-lwa',
version: '0.0.0',
params: {
title: 'CUTOS LWA',
host: 'localhost'
}
}
export let config: LwaConfig = { ...defaultConfig, params: { ...defaultConfig.params } }
function parse(str?: string) {
if (!str || !str.length) return {}
return str.split('&').reduce<Record<string, string>>((acc, row) => {
const [key, value] = row.split('=').map(decodeURIComponent)
acc[key] = value
return acc
}, {})
}
function mergeQueryParams(config: LwaConfig) {
if (typeof window === 'undefined') {
return config
}
const query = location.href.split('?')[1]
const { params } = parse(query) as { params?: string }
try {
if (params) config.params = { ...config.params, ...JSON.parse(params) }
} catch (error) {
console.error(error)
}
return config
}
export async function loadConfig(): Promise<LwaConfig> {
try {
const configUrl =
typeof window === 'undefined' ? 'config.json' : new URL('config.json', window.location.href).toString()
const response = await fetch(configUrl, { cache: 'no-cache' })
if (!response.ok) {
throw new Error(`Failed to load config.json: ${response.status}`)
}
config = mergeQueryParams((await response.json()) as LwaConfig)
return config
} catch (error) {
console.error(error)
config = mergeQueryParams({ ...defaultConfig, params: { ...defaultConfig.params } })
return config
}
}
+9
View File
@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_CUTOS_BROKER_URL?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {}
},
plugins: []
}
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"jsx": "preserve",
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.vue"]
}
@@ -0,0 +1,49 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
base: './',
publicDir: 'public',
build: {
target: ['chrome74']
},
server: {
proxy: {
// '/proxy': 'http://192.168.1.30'
}
},
resolve: {
dedupe: ['@cutos/core'],
alias: {
'@': '/src'
}
},
define: {},
plugins: [
vue(),
AutoImport({
include: [
/\.[tj]sx?$/,
/\.vue$/,
/\.vue\?vue/,
/\.md$/
],
imports: ['vue'],
vueTemplate: true,
cache: true,
dts: false
})
],
optimizeDeps: {
esbuildOptions: {
define: {
global: 'globalThis'
},
target: 'es2015',
supported: {
bigint: true
}
}
}
})