@hanzogui/vite-plugin 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugin.ts ADDED
@@ -0,0 +1,560 @@
1
+ import type { GuiOptions, ExtractedResponse } from '@hanzogui/static-worker'
2
+ import * as Static from '@hanzogui/static-worker'
3
+ import { getPragmaOptions } from '@hanzogui/static-worker'
4
+ import { createHash } from 'node:crypto'
5
+ import { existsSync } from 'node:fs'
6
+ import path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import type { Plugin, PluginOption, ResolvedConfig, ViteDevServer } from 'vite'
9
+ import { normalizePath, transformWithEsbuild, type Environment } from 'vite'
10
+ import {
11
+ loadGuiBuildConfig,
12
+ getLoadPromise,
13
+ getGuiOptions,
14
+ ensureFullConfigLoaded,
15
+ } from './loadGui'
16
+
17
+ const resolve = (name: string) => fileURLToPath(import.meta.resolve(name))
18
+
19
+ // shared cache across all plugin instances/environments via globalThis
20
+ type CacheEntry = {
21
+ js: string
22
+ map: any
23
+ cssImport: string | null
24
+ }
25
+
26
+ const CACHE_KEY = '__gui_vite_cache__'
27
+ const CACHE_SIZE_KEY = '__gui_vite_cache_size__'
28
+ const PENDING_KEY = '__gui_vite_pending__'
29
+
30
+ function getSharedCache(): Record<string, CacheEntry> {
31
+ if (!(globalThis as any)[CACHE_KEY]) {
32
+ ;(globalThis as any)[CACHE_KEY] = {}
33
+ }
34
+ return (globalThis as any)[CACHE_KEY]
35
+ }
36
+
37
+ function getSharedCacheSize(): number {
38
+ return (globalThis as any)[CACHE_SIZE_KEY] || 0
39
+ }
40
+
41
+ function setSharedCacheSize(size: number) {
42
+ ;(globalThis as any)[CACHE_SIZE_KEY] = size
43
+ }
44
+
45
+ function clearSharedCache() {
46
+ ;(globalThis as any)[CACHE_KEY] = {}
47
+ ;(globalThis as any)[CACHE_SIZE_KEY] = 0
48
+ }
49
+
50
+ // pending extractions map - dedupes concurrent requests for same file
51
+ function getPendingExtractions(): Map<string, Promise<CacheEntry | null>> {
52
+ if (!(globalThis as any)[PENDING_KEY]) {
53
+ ;(globalThis as any)[PENDING_KEY] = new Map()
54
+ }
55
+ return (globalThis as any)[PENDING_KEY]
56
+ }
57
+
58
+ type AliasOptions = {
59
+ /** use @hanzogui/react-native-web-lite, 'without-animated' for smaller bundle */
60
+ rnwLite?: boolean | 'without-animated'
61
+ /** alias react-native-svg to @hanzogui/react-native-svg */
62
+ svg?: boolean
63
+ }
64
+
65
+ type AliasEntry = { find: string | RegExp; replacement: string }
66
+
67
+ /**
68
+ * returns vite-compatible aliases for gui
69
+ * use this when you need control over alias ordering in your config
70
+ */
71
+ export function guiAliases(options: AliasOptions = {}): AliasEntry[] {
72
+ const aliases: AliasEntry[] = []
73
+
74
+ if (options.svg) {
75
+ aliases.push({
76
+ find: 'react-native-svg',
77
+ replacement: resolve('@hanzogui/react-native-svg'),
78
+ })
79
+ }
80
+
81
+ if (options.rnwLite) {
82
+ // entry point for main import (may be without-animated variant)
83
+ const rnwl = resolve(
84
+ options.rnwLite === 'without-animated'
85
+ ? '@hanzogui/react-native-web-lite/without-animated'
86
+ : '@hanzogui/react-native-web-lite'
87
+ )
88
+ // base package path for subpath imports (package directory, not entry file)
89
+ const rnwlBase = path.dirname(resolve('@hanzogui/react-native-web-lite/package.json'))
90
+ aliases.push(
91
+ {
92
+ // map deep RNW paths like dist/exports/StyleSheet/preprocess to rnw-lite's flat structure
93
+ // extracts the final path segment (e.g. "preprocess" or "createReactDOMStyle")
94
+ find: /^react-native(?:-web)?\/dist\/(?:exports|modules)\/.*\/([^/]+)$/,
95
+ replacement: `${rnwlBase}/dist/esm/$1.mjs`,
96
+ },
97
+ {
98
+ find: /^react-native$/,
99
+ replacement: rnwl,
100
+ },
101
+ {
102
+ find: /^react-native\/(Libraries\/Utilities\/codegenNativeComponent|Libraries\/Utilities\/codegenNativeCommand)$/,
103
+ replacement: `${rnwlBase}/$1`,
104
+ },
105
+ {
106
+ find: 'react-native/package.json',
107
+ replacement: resolve('@hanzogui/react-native-web-lite/package.json'),
108
+ },
109
+ {
110
+ find: /^react-native-web$/,
111
+ replacement: rnwl,
112
+ }
113
+ )
114
+ }
115
+
116
+ return aliases
117
+ }
118
+
119
+ export function guiPlugin({
120
+ disableResolveConfig,
121
+ ...guiOptionsIn
122
+ }: GuiOptions & {
123
+ disableResolveConfig?: boolean
124
+ } = {}): PluginOption {
125
+ // extraction ON by default, set disableExtraction: true to opt out
126
+ let shouldExtract = !guiOptionsIn.disableExtraction
127
+ let watcher: Promise<{ dispose: () => void } | void | undefined> | undefined
128
+
129
+ // TODO temporary fix
130
+ const enableNativeEnv = !!globalThis.__vxrnEnableNativeEnv
131
+
132
+ const extensions = [
133
+ `.web.mjs`,
134
+ `.web.js`,
135
+ `.web.jsx`,
136
+ `.web.ts`,
137
+ `.web.tsx`,
138
+ '.mjs',
139
+ '.js',
140
+ '.mts',
141
+ '.ts',
142
+ '.jsx',
143
+ '.tsx',
144
+ '.json',
145
+ ]
146
+
147
+ // start loading immediately but don't block
148
+ loadGuiBuildConfig(guiOptionsIn)
149
+
150
+ // helper to await load when needed
151
+ const ensureLoaded = async () => {
152
+ const promise = getLoadPromise()
153
+ if (promise) await promise
154
+ const options = getGuiOptions()
155
+ // update shouldExtract from loaded config (gui.build.ts)
156
+ if (options) {
157
+ shouldExtract = !options.disableExtraction
158
+ }
159
+ return options
160
+ }
161
+
162
+ // extract plugin state
163
+ const getHash = (input: string) => createHash('sha1').update(input).digest('base64')
164
+
165
+ // use shared cache across environments
166
+ const memoryCache = getSharedCache()
167
+
168
+ const cssMap = new Map<string, string>()
169
+ let config: ResolvedConfig
170
+ let server: ViteDevServer
171
+ const virtualExt = `.gui.css`
172
+
173
+ const getAbsoluteVirtualFileId = (filePath: string) => {
174
+ if (filePath.startsWith(config.root)) {
175
+ return filePath
176
+ }
177
+ return normalizePath(path.join(config.root, filePath))
178
+ }
179
+
180
+ function isNotClient(environment?: Environment) {
181
+ return environment?.name && environment.name !== 'client'
182
+ }
183
+
184
+ function isNative(environment?: Environment) {
185
+ return (
186
+ environment?.name && (environment.name === 'ios' || environment.name === 'android')
187
+ )
188
+ }
189
+
190
+ function invalidateModule(absoluteId: string) {
191
+ if (!server) return
192
+
193
+ const { moduleGraph } = server
194
+ const modules = moduleGraph.getModulesByFile(absoluteId)
195
+
196
+ if (modules) {
197
+ for (const module of modules) {
198
+ moduleGraph.invalidateModule(module)
199
+ module.lastHMRTimestamp = module.lastInvalidationTimestamp || Date.now()
200
+ }
201
+ }
202
+ }
203
+
204
+ const basePlugin: Plugin = {
205
+ name: 'hanzo-gui',
206
+ enforce: 'pre',
207
+
208
+ configureServer(_server) {
209
+ server = _server
210
+ },
211
+
212
+ async buildEnd() {
213
+ await watcher?.then((res) => {
214
+ res?.dispose()
215
+ })
216
+ },
217
+
218
+ async transform(code, id) {
219
+ if (id.includes('expo-linear-gradient')) {
220
+ return transformWithEsbuild(code, id, {
221
+ loader: 'jsx',
222
+ jsx: 'automatic',
223
+ })
224
+ }
225
+ },
226
+
227
+ async config(_, env) {
228
+ const options = await ensureLoaded()
229
+
230
+ if (!options) {
231
+ throw new Error(`No hanzo-gui options loaded`)
232
+ }
233
+
234
+ // start watching config if enabled
235
+ if (!options.disableWatchGuiConfig) {
236
+ watcher = Static.watchGuiConfig({
237
+ components: ['@hanzo/gui'],
238
+ config: './src/gui.config.ts',
239
+ ...options,
240
+ }).catch((err) => {
241
+ console.error(` [Hanzo GUI] Error watching config: ${err}`)
242
+ })
243
+ }
244
+
245
+ return {
246
+ envPrefix: ['HANZO_GUI_'],
247
+
248
+ environments: {
249
+ client: {
250
+ define: {
251
+ 'process.env.HANZO_GUI_IS_CLIENT': JSON.stringify(true),
252
+ 'process.env.HANZO_GUI_ENVIRONMENT': '"client"',
253
+ },
254
+ },
255
+ },
256
+
257
+ define: {
258
+ // reanimated support
259
+ _frameTimestamp: undefined,
260
+ _WORKLET: false,
261
+ __DEV__: `${env.mode === 'development'}`,
262
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || env.mode),
263
+ 'process.env.ENABLE_RSC': JSON.stringify(process.env.ENABLE_RSC || ''),
264
+ 'process.env.ENABLE_STEPS': JSON.stringify(process.env.ENABLE_STEPS || ''),
265
+ 'process.env.IS_STATIC': JSON.stringify(false),
266
+ ...(env.mode === 'production' && {
267
+ 'process.env.HANZO_GUI_OPTIMIZE_THEMES': JSON.stringify(true),
268
+ }),
269
+ },
270
+ resolve:
271
+ disableResolveConfig || enableNativeEnv
272
+ ? {}
273
+ : {
274
+ extensions,
275
+ alias: {
276
+ ...(options.platform !== 'native' && {
277
+ 'react-native/Libraries/Renderer/shims/ReactFabric':
278
+ resolve('@hanzogui/proxy-worm'),
279
+ 'react-native/Libraries/Utilities/codegenNativeComponent':
280
+ resolve('@hanzogui/proxy-worm'),
281
+ 'react-native-svg': resolve('@hanzogui/react-native-svg'),
282
+ ...(!options?.useReactNativeWebLite && {
283
+ 'react-native': resolve('react-native-web'),
284
+ }),
285
+ }),
286
+ },
287
+ },
288
+ }
289
+ },
290
+ }
291
+
292
+ const rnwLitePlugin: Plugin = {
293
+ name: 'gui-rnw-lite',
294
+
295
+ config() {
296
+ if (enableNativeEnv) {
297
+ return {}
298
+ }
299
+
300
+ const options = getGuiOptions()
301
+ if (!options?.useReactNativeWebLite) {
302
+ return {}
303
+ }
304
+
305
+ return {
306
+ resolve: {
307
+ alias: guiAliases({ rnwLite: options.useReactNativeWebLite }),
308
+ },
309
+ }
310
+ },
311
+ }
312
+
313
+ // extract plugin for optimize mode
314
+ // always included, but checks shouldExtract dynamically after config loads
315
+ const extractPlugin: Plugin = {
316
+ name: 'gui-extract',
317
+ enforce: 'pre',
318
+
319
+ async config(userConf) {
320
+ // wait for config to load to know if we should extract
321
+ const options = await ensureLoaded()
322
+
323
+ userConf.optimizeDeps ||= {}
324
+ userConf.optimizeDeps.include ||= []
325
+
326
+ // inline-style-prefixer is CJS with __esModule and breaks without pre-bundling
327
+ // (ReferenceError: exports is not defined). always include it.
328
+ userConf.optimizeDeps.include.push('inline-style-prefixer')
329
+
330
+ if (!shouldExtract) return
331
+
332
+ userConf.optimizeDeps.include.push('@hanzogui/core/inject-styles')
333
+ },
334
+
335
+ async configResolved(resolvedConfig) {
336
+ config = resolvedConfig
337
+ },
338
+
339
+ async resolveId(source) {
340
+ if (!shouldExtract) return
341
+
342
+ if (isNative(this.environment)) {
343
+ return
344
+ }
345
+
346
+ if (isNotClient(this.environment)) {
347
+ return
348
+ }
349
+
350
+ const [validId, query] = source.split('?')
351
+
352
+ if (!validId.endsWith(virtualExt)) {
353
+ return
354
+ }
355
+
356
+ const absoluteId = source.startsWith(config.root)
357
+ ? source
358
+ : getAbsoluteVirtualFileId(validId)
359
+
360
+ if (cssMap.has(absoluteId)) {
361
+ return absoluteId + (query ? `?${query}` : '')
362
+ }
363
+ },
364
+
365
+ async load(id) {
366
+ if (!shouldExtract) return
367
+
368
+ const options = getGuiOptions()
369
+ if (options?.disable) {
370
+ return
371
+ }
372
+
373
+ if (isNative(this.environment)) {
374
+ return
375
+ }
376
+
377
+ if (isNotClient(this.environment)) {
378
+ return
379
+ }
380
+
381
+ const [validId] = id.split('?')
382
+ return cssMap.get(validId)
383
+ },
384
+
385
+ transform: {
386
+ order: 'pre',
387
+ async handler(code, id) {
388
+ // ensure hanzo-gui is loaded before transform
389
+ const options = await ensureLoaded()
390
+
391
+ // ensure full config (heavy bundling) is loaded before extraction
392
+ await ensureFullConfigLoaded()
393
+
394
+ // fully disabled = no extraction AND no debug attrs
395
+ if (options?.disable) {
396
+ return
397
+ }
398
+
399
+ if (isNative(this.environment)) {
400
+ return
401
+ }
402
+
403
+ const [validId] = id.split('?')
404
+ if (!validId.endsWith('.tsx')) {
405
+ return
406
+ }
407
+
408
+ const { shouldDisable, shouldPrintDebug } = await getPragmaOptions({
409
+ source: code,
410
+ path: validId,
411
+ })
412
+
413
+ if (shouldPrintDebug) {
414
+ console.trace(
415
+ `Current file: ${id} in environment: ${this.environment?.name}, shouldDisable: ${shouldDisable}`
416
+ )
417
+ console.info(`\n\nOriginal source:\n${code}\n\n`)
418
+ }
419
+
420
+ if (shouldDisable) {
421
+ return
422
+ }
423
+
424
+ const isSSR = isNotClient(this.environment)
425
+
426
+ // cache key without environment - share compiled JS between SSR/client
427
+ const cacheKey = getHash(`${code}${id}`)
428
+ const pending = getPendingExtractions()
429
+
430
+ // helper to format result based on environment
431
+ const formatResult = (entry: CacheEntry) => {
432
+ const finalCode =
433
+ !isSSR && entry.cssImport ? `${entry.js}\n${entry.cssImport}` : entry.js
434
+ return { code: finalCode, map: entry.map }
435
+ }
436
+
437
+ // check cache first
438
+ const cached = memoryCache[cacheKey]
439
+ if (cached) {
440
+ if (process.env.DEBUG_HANZO_GUI_CACHE) {
441
+ console.info(
442
+ `[gui-cache] HIT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
443
+ )
444
+ }
445
+ return formatResult(cached)
446
+ }
447
+
448
+ // check if another request is already extracting this file
449
+ const pendingExtraction = pending.get(cacheKey)
450
+ if (pendingExtraction) {
451
+ if (process.env.DEBUG_HANZO_GUI_CACHE) {
452
+ console.info(
453
+ `[gui-cache] WAIT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
454
+ )
455
+ }
456
+ const result = await pendingExtraction
457
+ if (result) {
458
+ return formatResult(result)
459
+ }
460
+ return
461
+ }
462
+
463
+ if (process.env.DEBUG_HANZO_GUI_CACHE) {
464
+ console.info(
465
+ `[gui-cache] EXTRACT ${this.environment?.name || 'unknown'} ${id.split('/').pop()} key=${cacheKey.slice(0, 8)}`
466
+ )
467
+ }
468
+
469
+ // create extraction promise and store it for deduplication
470
+ const extractionPromise = (async (): Promise<CacheEntry | null> => {
471
+ let extracted: ExtractedResponse | null
472
+ try {
473
+ extracted = await Static!.extractToClassNames({
474
+ source: code,
475
+ sourcePath: validId,
476
+ options: options!,
477
+ shouldPrintDebug,
478
+ })
479
+ } catch (err) {
480
+ if (process.env.DEBUG_HANZO_GUI_CACHE) {
481
+ console.info(
482
+ `[gui-cache] ERROR extracting ${id.split('/').pop()}:`,
483
+ err
484
+ )
485
+ }
486
+ console.error(err instanceof Error ? err.message : String(err))
487
+ return null
488
+ }
489
+
490
+ if (!extracted) {
491
+ if (process.env.DEBUG_HANZO_GUI_CACHE) {
492
+ console.info(
493
+ `[gui-cache] no extraction result for ${id.split('/').pop()}`
494
+ )
495
+ }
496
+ return null
497
+ }
498
+
499
+ const rootRelativeId = `${validId}${virtualExt}`
500
+ const absoluteId = getAbsoluteVirtualFileId(rootRelativeId)
501
+
502
+ let cssImport: string | null = null
503
+
504
+ // store CSS and prepare import (but don't include in cached JS)
505
+ if (extracted.styles) {
506
+ this.addWatchFile(rootRelativeId)
507
+
508
+ if (server && cssMap.has(absoluteId)) {
509
+ invalidateModule(rootRelativeId)
510
+ }
511
+
512
+ cssImport = `import "${rootRelativeId}";`
513
+ cssMap.set(absoluteId, extracted.styles)
514
+ }
515
+
516
+ // cache the JS separately from CSS import
517
+ const jsCode = extracted.js.toString()
518
+ const cacheEntry: CacheEntry = {
519
+ js: jsCode,
520
+ map: extracted.map,
521
+ cssImport,
522
+ }
523
+
524
+ // track cache size and clear if too large (64MB)
525
+ const newSize = getSharedCacheSize() + jsCode.length
526
+ if (newSize > 67108864) {
527
+ clearSharedCache()
528
+ } else {
529
+ setSharedCacheSize(newSize)
530
+ }
531
+ memoryCache[cacheKey] = cacheEntry
532
+
533
+ if (process.env.DEBUG_HANZO_GUI_CACHE) {
534
+ console.info(
535
+ `[gui-cache] WRITE key=${cacheKey.slice(0, 8)} cacheSize=${Object.keys(memoryCache).length}`
536
+ )
537
+ }
538
+
539
+ return cacheEntry
540
+ })()
541
+
542
+ // store pending promise for deduplication
543
+ pending.set(cacheKey, extractionPromise)
544
+
545
+ try {
546
+ const result = await extractionPromise
547
+ if (result) {
548
+ return formatResult(result)
549
+ }
550
+ return
551
+ } finally {
552
+ // clean up pending map
553
+ pending.delete(cacheKey)
554
+ }
555
+ },
556
+ },
557
+ }
558
+
559
+ return [basePlugin, rnwLitePlugin, extractPlugin]
560
+ }
@@ -0,0 +1,2 @@
1
+ export declare const extensions: string[];
2
+ //# sourceMappingURL=extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,UAWtB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export * from './plugin';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA"}
@@ -0,0 +1,15 @@
1
+ import type { GuiOptions } from '@hanzogui/types';
2
+ export declare function getGuiOptions(): GuiOptions | null;
3
+ export declare function getLoadPromise(): Promise<GuiOptions> | null;
4
+ /**
5
+ * Load just the gui.build.ts config (lightweight)
6
+ * This doesn't bundle the full gui config - call ensureFullConfigLoaded() for that
7
+ */
8
+ export declare function loadGuiBuildConfig(optionsIn?: Partial<GuiOptions>): Promise<GuiOptions>;
9
+ /**
10
+ * Ensure the full gui config is loaded (heavy - bundles config + components)
11
+ * Call this lazily when transform/extraction is actually needed
12
+ */
13
+ export declare function ensureFullConfigLoaded(): Promise<void>;
14
+ export declare function cleanup(): Promise<void>;
15
+ //# sourceMappingURL=loadGui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadGui.d.ts","sourceRoot":"","sources":["../src/loadGui.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAwBjD,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AAED,wBAAgB,cAAc,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAE3D;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC9B,OAAO,CAAC,UAAU,CAAC,CAgBrB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAuB5D;AAED,wBAAsB,OAAO,kBAO5B"}
@@ -0,0 +1,22 @@
1
+ import type { GuiOptions } from '@hanzogui/static-worker';
2
+ import type { PluginOption } from 'vite';
3
+ type AliasOptions = {
4
+ /** use @hanzogui/react-native-web-lite, 'without-animated' for smaller bundle */
5
+ rnwLite?: boolean | 'without-animated';
6
+ /** alias react-native-svg to @hanzogui/react-native-svg */
7
+ svg?: boolean;
8
+ };
9
+ type AliasEntry = {
10
+ find: string | RegExp;
11
+ replacement: string;
12
+ };
13
+ /**
14
+ * returns vite-compatible aliases for gui
15
+ * use this when you need control over alias ordering in your config
16
+ */
17
+ export declare function guiAliases(options?: AliasOptions): AliasEntry[];
18
+ export declare function guiPlugin({ disableResolveConfig, ...guiOptionsIn }?: GuiOptions & {
19
+ disableResolveConfig?: boolean;
20
+ }): PluginOption;
21
+ export {};
22
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,yBAAyB,CAAA;AAO5E,OAAO,KAAK,EAAU,YAAY,EAAiC,MAAM,MAAM,CAAA;AAkD/E,KAAK,YAAY,GAAG;IAClB,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAAA;IACtC,2DAA2D;IAC3D,GAAG,CAAC,EAAE,OAAO,CAAA;CACd,CAAA;AAED,KAAK,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhE;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,YAAiB,GAAG,UAAU,EAAE,CA8CnE;AAED,wBAAgB,SAAS,CAAC,EACxB,oBAAoB,EACpB,GAAG,YAAY,EAChB,GAAE,UAAU,GAAG;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC1B,GAAG,YAAY,CAobpB"}